eBook – Guide Spring Cloud – NPI EA (cat=Spring Cloud)
announcement - icon

Let's get started with a Microservice Architecture with Spring Cloud:

>> Join Pro and download the eBook

eBook – Mockito – NPI EA (tag = Mockito)
announcement - icon

Mocking is an essential part of unit testing, and the Mockito library makes it easy to write clean and intuitive unit tests for your Java code.

Get started with mocking and improve your application tests using our Mockito guide:

Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Reactive – NPI EA (cat=Reactive)
announcement - icon

Spring 5 added support for reactive programming with the Spring WebFlux module, which has been improved upon ever since. Get started with the Reactor project basics and reactive programming in Spring Boot:

>> Join Pro and download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Jackson – NPI EA (cat=Jackson)
announcement - icon

Do JSON right with Jackson

Download the E-book

eBook – HTTP Client – NPI EA (cat=Http Client-Side)
announcement - icon

Get the most out of the Apache HTTP Client

Download the E-book

eBook – Maven – NPI EA (cat = Maven)
announcement - icon

Get Started with Apache Maven:

Download the E-book

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

eBook – RwS – NPI EA (cat=Spring MVC)
announcement - icon

Building a REST API with Spring?

Download the E-book

Course – LS – NPI EA (cat=Jackson)
announcement - icon

Get started with Spring and Spring Boot, through the Learn Spring course:

>> LEARN SPRING
Course – RWSB – NPI EA (cat=REST)
announcement - icon

Explore Spring Boot 3 and Spring 6 in-depth through building a full REST API with the framework:

>> The New “REST With Spring Boot”

Course – LSS – NPI EA (cat=Spring Security)
announcement - icon

Yes, Spring Security can be complex, from the more advanced functionality within the Core to the deep OAuth support in the framework.

I built the security material as two full courses - Core and OAuth, to get practical with these more complex scenarios. We explore when and how to use each feature and code through it on the backing project.

You can explore the course here:

>> Learn Spring Security

Course – LSD – NPI EA (tag=Spring Data JPA)
announcement - icon

Spring Data JPA is a great way to handle the complexity of JPA with the powerful simplicity of Spring Boot.

Get started with Spring Data JPA through the guided reference course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (cat=Spring Boot)
announcement - icon

Refactor Java code safely — and automatically — with OpenRewrite.

Refactoring big codebases by hand is slow, risky, and easy to put off. That’s where OpenRewrite comes in. The open-source framework for large-scale, automated code transformations helps teams modernize safely and consistently.

Each month, the creators and maintainers of OpenRewrite at Moderne run live, hands-on training sessions — one for newcomers and one for experienced users. You’ll see how recipes work, how to apply them across projects, and how to modernize code with confidence.

Join the next session, bring your questions, and learn how to automate the kind of work that usually eats your sprint time.

Course – Summer Sale 2026 – NPI EA (cat= Baeldung)
announcement - icon

Yes, we're now running our only Summer Sale. All Courses are 30% off until 20th July, 2026:

>> EXPLORE ACCESS NOW

Course – Summer Sale 2026 – NPI (cat=Baeldung)
announcement - icon

Yes, we're now running our only Summer Sale. All Courses are 30% off until 20th July, 2026:

>> EXPLORE ACCESS NOW

1. Introduction

In a previous tutorial, we covered the basics of JDBI, an open-source library for relational database access that removes much of the boilerplate code related to direct JDBC usage.

This time, we’ll see how we can use JDBI  in a Spring Boot application. We’ll also cover some aspects of this library that make it a good alternative to Spring Data JPA in some scenarios.

2. Project Setup

First of all, let’s add the appropriate JDBI dependencies to our project. This time, we’ll use JDBI’s Spring integration plugin, which brings all required core dependencies. We’ll also bring in the SqlObject plugin, which adds some extra features to base JDBI that we’ll use in our examples:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
    <version>3.0.5</version>
</dependency>
<dependency>
    <groupId>org.jdbi</groupId>
    <artifactId>jdbi3-spring5</artifactId>
    <version>3.38.0</version>
</dependency>
<dependency>
    <groupId>org.jdbi</groupId>
    <artifactId>jdbi3-sqlobject</artifactId>
    <version>3.38.0</version> 
</dependency>

The latest version of those artifacts can be found in Maven Central:

We also need a suitable JDBC driver to access our database. In this article we’ll use H2, so we must add its driver to our dependencies list as well:

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>2.1.214</version>
    <scope>runtime</scope>
</dependency>

3. JDBI Instantiation and Configuration

We’ve already seen in our previous article that we need a Jdbi instance as our entry point to access JDBI’s API. As we’re in the Spring world, it makes sense to make an instance of this class available as a bean.

We’ll leverage Spring Boot’s auto-configuration capabilities to initialize a DataSource and pass it to a @Bean-annotated method which will create our global Jdbi instance.

We’ll also pass any discovered plugins and RowMapper instances to this method so that they’re registered upfront:

@Configuration
public class JdbiConfiguration {
    @Bean
    public Jdbi jdbi(DataSource ds, List<JdbiPlugin> jdbiPlugins, List<RowMapper<?>> rowMappers) {        
        TransactionAwareDataSourceProxy proxy = new TransactionAwareDataSourceProxy(ds);        
        Jdbi jdbi = Jdbi.create(proxy);

        // Register all available plugins
        log.info("[I27] Installing plugins... ({} found)", jdbiPlugins.size());
        jdbiPlugins.forEach(jdbi::installPlugin);

        // Register all available rowMappers
        log.info("[I31] Installing rowMappers... ({} found)", rowMappers.size());
        rowMappers.forEach(jdbi::registerRowMapper);
        return jdbi;
    }
}

Here, we’re using an available DataSource and wrapping it in a TransactionAwareDataSourceProxy. We need this wrapper in order to integrate Spring-managed transactions with JDBI, as we’ll see later.

Registering plugins and RowMapper instances is straightforward. All we have to do is call installPlugin and installRowMapper for every available JdbiPlugin and RowMapper, respectively. After that, we have a fully configured Jdbi instance that we can use in our application.

4. Sample Domain

Our example uses a very simple domain model consisting of just two classes: CarMaker and CarModel. Since JDBI does not require any annotations on our domain classes, we can use simple POJOs:

public class CarMaker {
    private Long id;
    private String name;
    private List<CarModel> models;
    // getters and setters ...
}

public class CarModel {
    private Long id;
    private String name;
    private Integer yearDate;
    private String sku;
    private Long makerId;
    // getters and setters ...
}

5. Creating DAOs

Now, let’s create Data Access Objects (DAOs) for our domain classes. JDBI SqlObject plugin offers an easy way to implement those classes, which resembles Spring Data’s way of dealing with this subject.

We just have to define an interface with a few annotations and, automagically, JDBI will handle all low-level stuff such as handling JDBC connections and creating/disposing of statements and ResultSets:

@UseClasspathSqlLocator
public interface CarMakerDao {
    @SqlUpdate
    @GetGeneratedKeys
    Long insert(@BindBean CarMaker carMaker);
    
    @SqlBatch("insert")
    @GetGeneratedKeys
    List<Long> bulkInsert(@BindBean List<CarMaker> carMakers);
    
    @SqlQuery
    CarMaker findById(Long id);
}

@UseClasspathSqlLocator
public interface CarModelDao {    
    @SqlUpdate
    @GetGeneratedKeys
    Long insert(@BindBean CarModel carModel);

    @SqlBatch("insert")
    @GetGeneratedKeys
    List<Long> bulkInsert(@BindBean List<CarModel> models);

    @SqlQuery
    CarModel findByMakerIdAndSku(@Bind("makerId") Long makerId, @Bind("sku") String sku );
}

Those interfaces are heavily annotated, so let’s take a quick look at each of them.

5.1. @UseClasspathSqlLocator

The @UseClasspathSqlLocator annotation tells JDBI that actual SQL statements associated with each method are located at external resource files. By default, JDBI will lookup a resource using the interface’s fully qualified name and method. For instance, given an interface’s FQN of a.b.c.Foo with a findById() method, JDBI will look for a resource named a/b/c/Foo/findById.sql.

This default behavior can be overridden for any given method by passing the resource name as the value for the @SqlXXX annotation.

5.2. @SqlUpdate/@SqlBatch/@SqlQuery

We use the @SqlUpdate@SqlBatch, and @SqlQuery annotations to mark data-access methods, which will be executed using the given parameters. Those annotations can take an optional string value, which will be the literal SQL statement to execute – including any named parameters – or when used with @UseClasspathSqlLocator, the resource name containing it.

@SqlBatch-annotated methods can have collection-like arguments and execute the same SQL statement for every available item in a single batch statement. In each of the above DAO classes, we have a bulkInsert method that illustrates its use. The main advantage of using batch statements is the added performance we can achieve when dealing with large data sets.

5.3. @GetGeneratedKeys

As the name implies, the @GetGeneratedKeys annotation allows us to recover any generated keys as a result of successful execution. It’s mostly used in insert statements where our database will auto-generate new identifiers and we need to recover them in our code.

5.4. @BindBean/@Bind

We use @BindBean and @Bind annotations to bind the named parameters in the SQL statement with method parameters. @BindBean uses standard bean conventions to extract properties from a POJO – including nested ones. @Bind uses the parameter name or the supplied value to map its value to a named parameter.

6. Using DAOs

To use those DAOs in our application, we have to instantiate them using one of the factory methods available in JDBI.

In a Spring context, the simplest way is to create a bean for every DAO using the onDemand method:

@Bean
public CarMakerDao carMakerDao(Jdbi jdbi) {        
    return jdbi.onDemand(CarMakerDao.class);       
}

@Bean
public CarModelDao carModelDao(Jdbi jdbi) {
    return jdbi.onDemand(CarModelDao.class);
}

The onDemand-created instance is thread-safe and uses a database connection only during a method call. Since JDBI we’ll use the supplied TransactionAwareDataSourceProxy, this means we can use it seamlessly with Spring-managed transactions.

While simple, the approach we’ve used here is far from ideal when we have to deal with more than a few tables. One way to avoid writing this kind of boilerplate code is to create a custom BeanFactory. Describing how to implement such a component is beyond the scope of this tutorial, though.

7. Transactional Services

Let’s use our DAO classes in a simple service class that creates a few CarModel instances given a CarMaker populated with models. First, we’ll check if the given CarMaker was previously saved, saving it to the database if needed. Then, we’ll insert every CarModel one by one.

If there’s a unique key violation (or some other error) at any point, the whole operation must fail and a full rollback should be performed.

JDBI provides a @Transaction annotation, but we can’t use it here as it is unaware of other resources that might be participating in the same business transaction. Instead, we’ll use Spring’s @Transactional annotation in our service method:

@Service
public class CarMakerService {
    
    private CarMakerDao carMakerDao;
    private CarModelDao carModelDao;

    public CarMakerService(CarMakerDao carMakerDao,CarModelDao carModelDao) {        
        this.carMakerDao = carMakerDao;
        this.carModelDao = carModelDao;
    }    
    
    @Transactional
    public int bulkInsert(CarMaker carMaker) {
        Long carMakerId;
        if (carMaker.getId() == null ) {
            carMakerId = carMakerDao.insert(carMaker);
            carMaker.setId(carMakerId);
        }
        carMaker.getModels().forEach(m -> {
            m.setMakerId(carMaker.getId());
            carModelDao.insert(m);
        });                
        return carMaker.getModels().size();
    }
}

The operation’s implementation itself is quite simple: we’re using the standard convention that a null value in the id field implies this entity has not yet been persisted to the database. If this is the case, we use the CarMakerDao instance injected in the constructor to insert a new record in the database and get the generated id.

Once we have the CarMaker‘s id, we iterate over the models, setting the makerId field for each one before saving it to the database.

All those database operations will happen using the same underlying connection and will be part of the same transaction. The trick here lies in the way we’ve tied JDBI to Spring using TransactionAwareDataSourceProxy and creating onDemand DAOs. When JDBI requests a new Connection, it will get an existing one associated with the current transaction, thus integrating its lifecycle to other resources that might be enrolled.

8. Conclusion

In this article, we’ve shown how to quickly integrate JDBI into a Spring Boot application. This is a powerful combination in scenarios where we can’t use Spring Data JPA for some reason but still want to use all other features such as transaction management, integration and so on.

The code backing this article is available on GitHub. Once you're logged in as a Baeldung Pro Member, start learning and coding on the project.
Baeldung Pro – NPI EA (cat = Baeldung)
announcement - icon

Baeldung Pro comes with both absolutely No-Ads as well as finally with Dark Mode, for a clean learning experience:

>> Explore a clean Baeldung

Once the early-adopter seats are all used, the price will go up and stay at $33/year.

eBook – HTTP Client – NPI EA (cat=HTTP Client-Side)
announcement - icon

The Apache HTTP Client is a very robust library, suitable for both simple and advanced use cases when testing HTTP endpoints. Check out our guide covering basic request and response handling, as well as security, cookies, timeouts, and more:

>> Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

Course – LS – NPI EA (cat=REST)

announcement - icon

Get started with Spring Boot and with core Spring, through the Learn Spring course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (tag=Refactoring)
announcement - icon

Modern Java teams move fast — but codebases don’t always keep up. Frameworks change, dependencies drift, and tech debt builds until it starts to drag on delivery. OpenRewrite was built to fix that: an open-source refactoring engine that automates repetitive code changes while keeping developer intent intact.

The monthly training series, led by the creators and maintainers of OpenRewrite at Moderne, walks through real-world migrations and modernization patterns. Whether you’re new to recipes or ready to write your own, you’ll learn practical ways to refactor safely and at scale.

If you’ve ever wished refactoring felt as natural — and as fast — as writing code, this is a good place to start.

Course – Summer Sale 2026 – NPI EA (cat= Baeldung)
announcement - icon

Yes, we're now running our only Summer Sale. All Courses are 30% off until 20th July, 2026:

>> EXPLORE ACCESS NOW

Course – Summer Sale 2026 – NPI (All)
announcement - icon

Yes, we're now running our only Summer Sale. All Courses are 30% off until 20th July, 2026:

>> EXPLORE ACCESS NOW

eBook Jackson – NPI EA – 3 (cat = Jackson)