Home Java Spring JDBC with JdbcTemplate: Database Access Done Right
Intermediate 5 min · July 14, 2026

Spring JDBC with JdbcTemplate: Database Access Done Right

Master Spring JdbcTemplate with production-tested patterns.

N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Notes here come from systems that actually shipped.

Follow
Production
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
Before you start⏱ 15-20 min read
  • Java 8 or higher
  • Basic understanding of SQL and JDBC
  • Spring Boot or Spring Framework project setup
  • A running database (e.g., H2, PostgreSQL, MySQL) for testing
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Use JdbcTemplate for simple CRUD and batch operations; it's lighter than JPA and gives you full SQL control.
  • Always use BeanPropertyRowMapper or custom RowMapper to avoid manual mapping errors.
  • Batch operations with batchUpdate() can be 10x faster than single inserts.
  • Never concatenate SQL strings; always use ? placeholders to prevent injection.
  • For complex queries, consider NamedParameterJdbcTemplate for readability.
✦ Definition~90s read
What is Spring JDBC with JdbcTemplate?

Spring JdbcTemplate is a central class in the Spring JDBC framework that simplifies database access by handling connection management, exception translation, and result mapping, allowing you to focus on SQL and business logic.

Think of JdbcTemplate as a power drill with safety features.
Plain-English First

Think of JdbcTemplate as a power drill with safety features. Raw JDBC is like using a manual screwdriver—it works but takes forever and you might strip the screws. JdbcTemplate automates the repetitive parts (opening/closing connections, handling exceptions) while still letting you control exactly how the screw goes in. You get the speed of a power tool without losing precision.

If you're building a payment processing system, you don't want an ORM deciding when to flush your transactions. I've seen production outages caused by JPA's auto-flush behavior at exactly the wrong moment. That's where JdbcTemplate shines—it gives you direct SQL control without the ceremony of raw JDBC.

Spring's JdbcTemplate has been battle-tested since Spring 1.0. It wraps JDBC with a clean API, handles resource management, and maps results to objects. But here's the hard truth: most teams misuse it. They either treat it like a toy (using it for everything when JPA would be better) or they over-engineer with unnecessary abstractions.

In this guide, I'll show you the patterns I've used across fintech, e-commerce, and SaaS platforms. We'll cover setup, CRUD operations, batch processing, error handling, and the gotchas that the official docs gloss over. By the end, you'll know exactly when to use JdbcTemplate—and when to walk away.

Setting Up JdbcTemplate: The Right Way

Let's cut through the noise. You need a DataSource and a JdbcTemplate bean. If you're using Spring Boot, auto-configuration does most of the work—just add the dependency and configure your datasource in application.properties. But here's what the docs won't tell you: always set a meaningful pool size and timeout. I once saw a team use the default HikariCP settings (pool size 10) and wonder why their batch jobs timed out.

First, add the dependency: ``xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> ``

Then configure your datasource. For production, never use spring.datasource.url without tuning the pool: ``properties spring.datasource.url=jdbc:postgresql://localhost:5432/mydb spring.datasource.username=user spring.datasource.password=pass spring.datasource.hikari.maximum-pool-size=20 spring.datasource.hikari.minimum-idle=5 spring.datasource.hikari.connection-timeout=30000 spring.datasource.hikari.idle-timeout=600000 ``

Now create your JdbcTemplate bean. If you have multiple datasources, qualify them with @Primary or @Qualifier: ```java @Configuration public class DatabaseConfig {

@Bean public JdbcTemplate jdbcTemplate(DataSource dataSource) { return new JdbcTemplate(dataSource); } } ```

That's it. You're ready to query. But remember: JdbcTemplate is thread-safe, so inject it as a singleton. Don't create a new instance per request—I've seen that anti-pattern in legacy code.

DatabaseConfig.javaJAVA
1
2
3
4
5
6
7
8
9
10
@Configuration
public class DatabaseConfig {

    @Bean
    public JdbcTemplate jdbcTemplate(DataSource dataSource) {
        JdbcTemplate jt = new JdbcTemplate(dataSource);
        jt.setFetchSize(1000); // adjust based on expected result size
        return jt;
    }
}
💡Set Fetch Size for Large Result Sets
📊 Production Insight
I once debugged a memory leak caused by not setting fetch size. The app queried 500k rows, loaded them all into heap, and OOM'd. Set fetch size to a reasonable value (e.g., 1000) to stream results.
🎯 Key Takeaway
Configure JdbcTemplate as a singleton bean with tuned pool settings and fetch size.

CRUD Operations with JdbcTemplate

Let's build a simple UserRepository using JdbcTemplate. I'll show you the patterns I use in production—no fluff.

Querying for a single object: ``java public User findById(Long id) { String sql = "SELECT id, name, email FROM users WHERE id = ?"; return jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(User.class), id); } ``

Querying for a list: ``java public List<User> findAll() { String sql = "SELECT id, name, email FROM users"; return jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(User.class)); } ``

Insert and get generated key: ``java public User save(User user) { String sql = "INSERT INTO users (name, email) VALUES (?, ?)"; KeyHolder keyHolder = new GeneratedKeyHolder(); jdbcTemplate.update(connection -> { PreparedStatement ps = connection.prepareStatement(sql, new String[]{"id"}); ps.setString(1, user.getName()); ps.setString(2, user.getEmail()); return ps; }, keyHolder); user.setId(keyHolder.getKey().longValue()); return user; } ``

Update and delete: ```java public int update(User user) { String sql = "UPDATE users SET name = ?, email = ? WHERE id = ?"; return jdbcTemplate.update(sql, user.getName(), user.getEmail(), user.getId()); }

public int deleteById(Long id) { String sql = "DELETE FROM users WHERE id = ?"; return jdbcTemplate.update(sql, id); } ```

Notice I use BeanPropertyRowMapper—it maps columns to fields by name. But be careful: if your column names don't match exactly (e.g., user_name vs userName), you'll get nulls. In that case, write a custom RowMapper or use aliases in SQL.

UserRepository.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Repository
public class UserRepository {

    private final JdbcTemplate jdbcTemplate;

    public UserRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    public User findById(Long id) {
        String sql = "SELECT id, name, email FROM users WHERE id = ?";
        return jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(User.class), id);
    }

    public List<User> findAll() {
        String sql = "SELECT id, name, email FROM users";
        return jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(User.class));
    }

    public User save(User user) {
        String sql = "INSERT INTO users (name, email) VALUES (?, ?)";
        KeyHolder keyHolder = new GeneratedKeyHolder();
        jdbcTemplate.update(connection -> {
            PreparedStatement ps = connection.prepareStatement(sql, new String[]{"id"});
            ps.setString(1, user.getName());
            ps.setString(2, user.getEmail());
            return ps;
        }, keyHolder);
        user.setId(keyHolder.getKey().longValue());
        return user;
    }

    public int update(User user) {
        String sql = "UPDATE users SET name = ?, email = ? WHERE id = ?";
        return jdbcTemplate.update(sql, user.getName(), user.getEmail(), user.getId());
    }

    public int deleteById(Long id) {
        String sql = "DELETE FROM users WHERE id = ?";
        return jdbcTemplate.update(sql, id);
    }
}
⚠ BeanPropertyRowMapper vs Underscore Naming
📊 Production Insight
I've seen teams use queryForObject when the query might return null. That throws EmptyResultDataAccessException. Always catch it or use query and check the list size.
🎯 Key Takeaway
Use BeanPropertyRowMapper for simple mappings, but be aware of naming differences. For insert, use KeyHolder to retrieve generated keys.

Batch Operations: Bulk Inserts and Updates

Batch operations are where JdbcTemplate really shines. If you're inserting thousands of rows one by one, you're doing it wrong. Use batchUpdate().

Batch insert with simple parameters: ``java public int[] batchInsert(List<User> users) { String sql = "INSERT INTO users (name, email) VALUES (?, ?)"; List<Object[]> batchArgs = new ArrayList<>(); for (User user : users) { batchArgs.add(new Object[]{user.getName(), user.getEmail()}); } return jdbcTemplate.batchUpdate(sql, batchArgs); } ``

Batch update with NamedParameterJdbcTemplate (cleaner): ``java public int[] batchInsertNamed(List<User> users) { String sql = "INSERT INTO users (name, email) VALUES (:name, :email)"; SqlParameterSource[] batch = SqlParameterSourceUtils.createBatch(users.toArray()); return namedParameterJdbcTemplate.batchUpdate(sql, batch); } ``

Important: Enable driver-level batching. For PostgreSQL, add ?reWriteBatchedInserts=true to your JDBC URL. Without it, batchUpdate() still sends individual INSERT statements. For MySQL, add ?rewriteBatchedStatements=true. This can give you a 10x performance boost.

Batch size matters. I've seen teams set batch size to 100,000 and wonder why the database stalls. Start with 500-1000 and tune based on your row size and network latency. Monitor the time it takes and adjust.

BatchExample.javaJAVA
1
2
3
4
5
6
7
8
public int[] batchInsert(List<User> users) {
    String sql = "INSERT INTO users (name, email) VALUES (?, ?)";
    List<Object[]> batchArgs = new ArrayList<>();
    for (User user : users) {
        batchArgs.add(new Object[]{user.getName(), user.getEmail()});
    }
    return jdbcTemplate.batchUpdate(sql, batchArgs);
}
Output
Returns int[] with number of affected rows per batch statement.
🔥NamedParameterJdbcTemplate for Readability
📊 Production Insight
In a fintech app, we processed 1M transactions nightly. Without reWriteBatchedInserts, it took 2 hours. With it, 12 minutes. Always check your driver's batching support.
🎯 Key Takeaway
Use batchUpdate() for bulk operations, enable driver-level batching, and keep batch sizes reasonable (500-1000).

What the Official Docs Won't Tell You

I've been using JdbcTemplate since Spring 2.5. Here are the gotchas that cost me weekends.

1. queryForObject throws EmptyResultDataAccessException when no rows are returned. The docs mention it, but they don't tell you that catching it is ugly. Instead, use query and check the list size, or use Optional: ``java public Optional<User> findById(Long id) { String sql = "SELECT id, name, email FROM users WHERE id = ?"; List<User> users = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(User.class), id); return users.isEmpty() ? Optional.empty() : Optional.of(users.get(0)); } ``

2. BeanPropertyRowMapper silently maps columns by name. If a column is missing or renamed, the field stays null—no error. Always test your mappings with a query that returns actual data. I once spent hours debugging why email was null after a schema migration.

3. batchUpdate() doesn't actually batch by default. The JDBC driver sends individual statements unless you enable driver-level batching. This is the most common performance trap. Always verify by enabling JDBC logging.

4. KeyHolder with PreparedStatementCreator is verbose but necessary. The lambda version is cleaner, but you must specify the column names for the generated keys. If you get -1 as the key, you forgot to pass the column names.

5. Transaction management is not automatic. JdbcTemplate doesn't participate in transactions unless you wrap calls with @Transactional. If you're using it outside a transaction, each call is auto-committed. This can lead to partial updates if you don't manage transactions explicitly.

6. Logging is your best friend. Enable logging.level.org.springframework.jdbc=DEBUG to see every SQL statement and parameter. This alone saves hours of debugging.

SafeQueryExample.javaJAVA
1
2
3
4
5
public Optional<User> findByIdSafe(Long id) {
    String sql = "SELECT id, name, email FROM users WHERE id = ?";
    List<User> users = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(User.class), id);
    return users.isEmpty() ? Optional.empty() : Optional.of(users.get(0));
}
⚠ Always Test RowMapper with Real Data
📊 Production Insight
I once saw a production issue where a column rename caused email to be null in all queries. The app didn't crash—it just sent emails to null addresses. Always validate your mappings.
🎯 Key Takeaway
Beware of silent failures: EmptyResultDataAccessException, null fields from mismatched columns, and non-batched batchUpdate.

Error Handling and Exception Translation

Spring's DataAccessException hierarchy is your friend. JdbcTemplate automatically translates SQLException into meaningful exceptions like DuplicateKeyException, DataIntegrityViolationException, and BadSqlGrammarException. But here's the rub: you need to handle them properly.

Don't catch generic Exception. Be specific: ``java try { jdbcTemplate.update(sql, params); } catch (DuplicateKeyException e) { // handle duplicate entry } catch (DataIntegrityViolationException e) { // handle constraint violation } catch (DataAccessException e) { // generic database error } ``

Use @Repository annotation to enable persistence exception translation. Without it, you get raw SQLException instead of Spring's exceptions. This is a common oversight.

Log the SQL and parameters when an exception occurs. I always include a utility method: ``java private void logError(String sql, Object[] params, DataAccessException e) { log.error("SQL: {} | Params: {}", sql, Arrays.toString(params), e); } ``

Consider using @Transactional with rollback rules. By default, runtime exceptions trigger rollback. Checked exceptions do not. If you want a checked exception to rollback, use @Transactional(rollbackFor = {CheckedException.class}).

ErrorHandlingExample.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Repository
public class UserRepository {

    public User save(User user) {
        String sql = "INSERT INTO users (name, email) VALUES (?, ?)";
        try {
            KeyHolder keyHolder = new GeneratedKeyHolder();
            jdbcTemplate.update(connection -> {
                PreparedStatement ps = connection.prepareStatement(sql, new String[]{"id"});
                ps.setString(1, user.getName());
                ps.setString(2, user.getEmail());
                return ps;
            }, keyHolder);
            user.setId(keyHolder.getKey().longValue());
            return user;
        } catch (DuplicateKeyException e) {
            throw new UserAlreadyExistsException("User with email " + user.getEmail() + " already exists", e);
        } catch (DataAccessException e) {
            logError(sql, new Object[]{user.getName(), user.getEmail()}, e);
            throw new DatabaseException("Failed to save user", e);
        }
    }
}
💡Always Log Parameters on Exception
📊 Production Insight
In a SaaS billing system, we caught DataIntegrityViolationException to handle duplicate invoice numbers gracefully. Without specific handling, the user would see a 500 error instead of a meaningful message.
🎯 Key Takeaway
Use @Repository for exception translation, catch specific DataAccessException subclasses, and always log SQL parameters on error.

When to Use JdbcTemplate vs JPA vs Spring Data JDBC

  • Use JdbcTemplate when you need fine-grained SQL control, batch operations, or when working with legacy databases where JPA mappings are painful. Also, if performance is critical and you can't afford ORM overhead.
  • Use Spring Data JPA when you have a standard domain model, CRUD operations, and want automatic query generation. It's great for rapid development.
  • Use Spring Data JDBC as a middle ground—it gives you aggregate-oriented persistence without JPA's complexity. Good for microservices with simple entities.

When NOT to use JdbcTemplate: - Complex object graphs with many relationships (use JPA). - When you need lazy loading or caching (JPA provides that). - If your team is more productive with JPA (but don't use JPA just because it's trendy).

Real-world example: In a high-frequency trading system, we used JdbcTemplate for all database access. JPA's caching and lazy loading caused inconsistent reads. JdbcTemplate gave us predictable performance.

ComparisonExample.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// JdbcTemplate: explicit SQL
public List<User> findActiveUsers() {
    return jdbcTemplate.query("SELECT * FROM users WHERE active = true", 
        new BeanPropertyRowMapper<>(User.class));
}

// Spring Data JPA: derived query
public interface UserRepository extends JpaRepository<User, Long> {
    List<User> findByActiveTrue();
}

// Spring Data JDBC: similar to JPA but without lazy loading
public interface UserRepository extends CrudRepository<User, Long> {
    List<User> findByActiveTrue();
}
🔥Don't Mix JPA and JdbcTemplate in the Same Transaction
📊 Production Insight
I've seen teams adopt JPA for everything and then struggle with N+1 queries. JdbcTemplate forces you to write explicit SQL, which often leads to more efficient queries.
🎯 Key Takeaway
JdbcTemplate is best for SQL control and performance; JPA for complex object graphs; Spring Data JDBC for simplicity. Choose based on your use case, not hype.
● Production incidentPOST-MORTEMseverity: high

The Batch Insert That Took Down a Billing System

Symptom
Users saw '500 Internal Server Error' when submitting invoices. The system became unresponsive for 45 minutes.
Assumption
The developer assumed that batchUpdate() in JdbcTemplate would automatically batch the inserts in a single database round-trip.
Root cause
The batch size was set to 10,000, and the underlying PostgreSQL connection pool had only 10 connections. Each batch consumed a connection for the entire duration of the insert, causing connection starvation. Additionally, the SQL was not using prepared statement batching correctly—each row was sent individually.
Fix
Reduced batch size to 500, ensured rewriteBatchedInserts=true in the PostgreSQL connection string, and increased the connection pool to 50. Also switched to NamedParameterJdbcTemplate with SqlParameterSourceUtils.createBatch() for cleaner code.
Key lesson
  • Always tune batch sizes to match database and connection pool capacity.
  • Enable driver-level batching for your database (e.g., rewriteBatchedInserts for PostgreSQL).
  • Monitor connection pool usage under load; a spike in active connections often indicates a batch issue.
  • Test batch operations with realistic data volumes in a staging environment.
  • Use NamedParameterJdbcTemplate for batch operations—it's less error-prone.
Production debug guideSymptom to Action5 entries
Symptom · 01
Queries return incomplete or wrong data
Fix
Check your RowMapper implementation. Common mistake: mapping columns by index instead of name, and the column order changed after an ALTER TABLE.
Symptom · 02
Slow batch inserts
Fix
Enable rewriteBatchedInserts for PostgreSQL/MySQL. Also verify that batchUpdate() is actually sending batched statements by enabling JDBC logging.
Symptom · 03
Connection pool exhaustion
Fix
Ensure every JdbcTemplate operation is wrapped in a try-with-resources or within a transaction scope. Check for unclosed ResultSet from custom RowMapper implementations.
Symptom · 04
SQL injection vulnerability warning
Fix
Search for string concatenation in SQL strings. Use ? placeholders or NamedParameterJdbcTemplate with named parameters.
Symptom · 05
Transaction not rolling back on exception
Fix
Verify that PlatformTransactionManager is configured and that @Transactional is on a public method called from outside the class. Also check for swallowed exceptions in catch blocks.
★ Quick Debug Cheat SheetFive common JdbcTemplate issues and immediate fixes.
Query returns empty result when data exists
Immediate action
Check if your RowMapper is correctly mapping columns. Use BeanPropertyRowMapper for simple cases.
Commands
jdbcTemplate.queryForList("SELECT * FROM users WHERE id = ?", 1); // returns List<Map<String, Object>>
jdbcTemplate.query("SELECT * FROM users WHERE id = ?", new BeanPropertyRowMapper<>(User.class), 1);
Fix now
Replace custom RowMapper with BeanPropertyRowMapper temporarily to isolate the issue.
Batch insert is slow+
Immediate action
Enable driver-level batching and reduce batch size.
Commands
jdbcTemplate.batchUpdate("INSERT INTO users (name) VALUES (?)", batchArgs);
Check connection string: jdbc:postgresql://host/db?reWriteBatchedInserts=true
Fix now
Set batch size to 100-500 and add rewriteBatchedInserts=true for PostgreSQL.
Data truncation or type conversion error+
Immediate action
Verify that your SQL types match the database schema. Use explicit type casting in SQL.
Commands
jdbcTemplate.update("UPDATE users SET name = ? WHERE id = ?", name, id);
jdbcTemplate.update("UPDATE users SET name = CAST(? AS VARCHAR(50)) WHERE id = ?", name, id);
Fix now
Add explicit CAST in SQL to match column types.
Transaction not rolling back+
Immediate action
Check if the method is public and called from outside the class. Verify transaction manager bean exists.
Commands
@Transactional public void saveUser() { ... }
jdbcTemplate.update("INSERT INTO users (name) VALUES (?)", name);
Fix now
Ensure @Transactional is on a public method and call it from another bean.
SQL injection vulnerability+
Immediate action
Replace string concatenation with parameterized queries.
Commands
jdbcTemplate.query("SELECT * FROM users WHERE name = '" + name + "'", ...); // BAD
jdbcTemplate.query("SELECT * FROM users WHERE name = ?", new Object[]{name}, ...); // GOOD
Fix now
Search for any SQL string concatenation and replace with placeholders.
FeatureJdbcTemplateSpring Data JPASpring Data JDBC
SQL ControlFull controlAbstracted via JPQLFull control
PerformanceHigh (no caching overhead)Moderate (caching can help/hinder)High
Complex RelationshipsManual mappingAutomaticLimited (aggregates only)
Learning CurveLow (if you know SQL)Medium (JPA concepts)Low
Batch OperationsExcellentModerateGood
⚙ Quick Reference
6 commands from this guide
FileCommand / CodePurpose
DatabaseConfig.java@ConfigurationSetting Up JdbcTemplate
UserRepository.java@RepositoryCRUD Operations with JdbcTemplate
BatchExample.javapublic int[] batchInsert(List users) {Batch Operations
SafeQueryExample.javapublic Optional findByIdSafe(Long id) {What the Official Docs Won't Tell You
ErrorHandlingExample.java@RepositoryError Handling and Exception Translation
ComparisonExample.javapublic List findActiveUsers() {When to Use JdbcTemplate vs JPA vs Spring Data JDBC

Key takeaways

1
JdbcTemplate is ideal for SQL-centric applications where performance and control matter more than ORM convenience.
2
Always use parameterized queries to prevent SQL injection and enable prepared statement caching.
3
Batch operations require driver-level batching (e.g., reWriteBatchedInserts=true) to realize performance gains.
4
Handle exceptions specifically and log SQL parameters for effective debugging.
5
Choose JdbcTemplate over JPA when you need explicit SQL, batch processing, or when working with legacy schemas.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
How does JdbcTemplate handle database connection management?
Q02SENIOR
What is the difference between query() and queryForObject() in JdbcTempl...
Q03SENIOR
How would you implement pagination with JdbcTemplate?
Q01 of 03JUNIOR

How does JdbcTemplate handle database connection management?

ANSWER
JdbcTemplate relies on a DataSource to obtain connections. It opens a connection for each operation (or reuses one from the pool), executes the SQL, and then closes the connection (returns it to the pool). It handles the lifecycle automatically, so developers don't need to manage connections manually.
FAQ · 3 QUESTIONS

Frequently Asked Questions

01
How do I handle null parameters in JdbcTemplate?
02
Can I use JdbcTemplate with stored procedures?
03
Is JdbcTemplate thread-safe?
N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Notes here come from systems that actually shipped.

Follow
Verified
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
🔥

That's Hibernate & JPA. Mark it forged?

5 min read · try the examples if you haven't

Previous
Hibernate N+1 Problem and How to Fix It
8 / 28 · Hibernate & JPA
Next
Introduction to Spring Data JDBC: Simplified Database Access Without JPA