Transparent Statement Caching in Modern JDBC Drivers
Most Java developers have written code like this thousands of times, or at least their preferred ORM frameworks have:
Transparent Statement Caching in Modern JDBC Drivers

Image showing how JDBC drivers can cache and reuse statements
Most Java developers have written code like this thousands of times, or at least their preferred ORM frameworks have:
PreparedStatement ps = connection.prepareStatement(
"select * from orders where customer_id = ?"
);
ps.setLong(1, customerId);
ResultSet rs = ps.executeQuery();
And after that, the responsible thing happens:
ps.close();
At first glance, this looks simple. The application creates a prepared statement, uses it, and closes it. Next request, same thing again. New PreparedStatement, same SQL, same database, same work repeated.
But modern JDBC drivers are often doing something more interesting under the covers.
In many cases, close() does not necessarily mean “throw everything away.” It can mean “the application is done with this statement, but the driver may keep reusable internal state for later.” That is the basic idea behind transparent statement caching.
It is called transparent because the application keeps using normal JDBC code. You still call connection.prepareStatement(sql). You still close the statement. You do not have to call a special “get statement from cache” API in the common case. The driver decides whether a previously prepared statement can be reused.
This is one of those JDBC features that can quietly help performance, but it is also easy to misunderstand.
A prepared statement is more than a Java object
When you create a PreparedStatement, you are not only creating a Java object.
Depending on the database and driver, preparing a statement may involve parsing SQL, describing parameters, checking metadata, allocating driver-side structures, and sometimes creating a server-side prepared statement handle. Some databases and drivers also separate the prepare phase from the execute phase more explicitly than others.
So if your application executes the same SQL many times, there is a good chance that repeating all of that setup work is wasteful.
That is where statement caching comes in.
The basic promise is simple: if the application repeatedly prepares the same SQL, the driver may reuse some of the statement-related work instead of starting from zero every time.
But the important word is some.
Statement caching does not mean every database will behave the same way. It also does not mean the query itself becomes fast. If your SQL scans a huge table, statement caching will not magically fix that. It mainly reduces the overhead around repeatedly preparing the same kind of statement.
A good mental model is this:
Statement caching does not make a slow query fast. It makes repeated statement setup cheaper.
Why “transparent” matters
Oracle JDBC provides a very clear example of the transparent model. Oracle’s documentation describes implicit statement caching, where the SQL string of a prepared or callable statement is used as the cache key and no special action is required from the application to retrieve the statement from the cache. If the driver cannot find a cached statement, it creates one automatically.
That is the part that matters for normal application code.
The developer writes:
connection.prepareStatement(sql);
The driver can internally ask:
“Have I seen this SQL before on this connection?”
If yes, it may reuse a cached statement. If not, it prepares a new one.
This is very different from the older mental model where every call to prepareStatement() is assumed to trigger a full prepare operation from scratch. With transparent caching enabled, the JDBC API usage looks the same, but the runtime behavior can be different.
Oracle also documents both implicit and explicit statement caching. The implicit version is the one most aligned with the word “transparent,” because it works through standard statement creation calls rather than requiring the application to manage cache keys manually.
The physical connection boundary
This is where many developers get caught.
Statement caches are usually tied to a physical JDBC connection.
That means a statement cached on connection A is not automatically available on connection B.
This matters because most Java applications do not use one database connection. They use a connection pool. If the pool has 30 physical connections, then conceptually you may have 30 separate statement-cache worlds.
PostgreSQL’s JDBC driver documentation is explicit about this: each connection has its own statement cache. It also says the cache lets the application benefit from server-prepared statements even if the prepared statement is closed after each execution.
That detail is important.
From the application’s point of view, the statement was closed. From the driver’s point of view, it may still remember enough to make the next execution cheaper, as long as the next execution happens on a connection where that SQL is cached.
This is also why connection-pool size and statement-cache size should not be tuned independently.
If you configure a cache of 100 statements per connection and your pool has 50 physical connections, you have not configured “100 cached statements.” You may have configured up to 5,000 cached statement entries across that application instance.
Now multiply that by ten service replicas.
Now multiply that by several microservices.
This is how a small-looking driver setting can become a meaningful amount of client-side and sometimes server-side state.
PostgreSQL: useful, but bounded for a reason
The PostgreSQL JDBC driver has several settings around prepared statement caching, including preparedStatementCacheQueries, preparedStatementCacheSizeMiB, and prepareThreshold. The driver documentation says server-prepared statements consume memory on both the client and server, so pgJDBC limits the number of server-prepared statements per connection. It also explains that only a subset of the statement cache becomes server-prepared, because some statements may not reach the configured prepareThreshold.
That tells us something useful: statement caching is not just a free optimization switch.
It is a tradeoff.
The driver keeps useful state so repeated SQL can be cheaper. But that state consumes memory. If server-prepared statements are involved, the database server may also hold resources for them.
This is why a bounded cache is normal. An unbounded statement cache would be dangerous in real systems, especially in applications that generate many distinct SQL strings.
The best case for statement caching is boring, repetitive SQL:
select * from orders where customer_id = ?
The bad case is dynamically generated SQL with values pasted directly into the string:
select * from orders where customer_id = 123
select * from orders where customer_id = 456
select * from orders where customer_id = 789
Those are different SQL strings. A cache that keys by SQL text will treat them as different entries.
The better form is:
select * from orders where customer_id = ?
Now the SQL shape is stable, and the value changes through bind parameters.
This is one of the quiet performance benefits of using prepared statements properly. You are not only avoiding SQL injection and making parameter handling cleaner. You are also giving the driver and database a better chance to reuse work.
MySQL Connector/J: caching is configurable
MySQL Connector/J exposes prepared statement cache settings such as cachePrepStmts, prepStmtCacheSize, and prepStmtCacheSqlLimit. The MySQL documentation describes prepStmtCacheSize as the number of prepared statements to cache when prepared statement caching is enabled, and prepStmtCacheSqlLimit as the largest SQL size the driver will cache parsing for.
That last setting is worth noticing.
Not every SQL string is equally good for caching. Very large SQL strings may be excluded. That makes sense. A cache should usually focus on statements that repeat often and are cheap enough to keep around.
This is another reason to avoid treating statement caching as magic. If the SQL generated by your ORM is huge, highly variable, or full of literal values, your cache may not help as much as expected.
The driver can only reuse what is reusable.
SQL Server JDBC: prepared statement handle caching
Microsoft’s JDBC driver also has statement pooling and prepared statement handle caching. The SQL Server JDBC documentation describes statementPoolingCacheSize as the property that defines the size of the cache for statement pooling. It also states that setting disableStatementPooling to true or setting statementPoolingCacheSize to 0 disables prepared statement handle caching.
This wording is useful because it shows another variation in terminology.
Some documentation calls it statement caching. Some calls it statement pooling. Some talks about prepared statement handle caching. These are not always identical implementations, but they live in the same general family: avoid unnecessary repeated prepare work when the same statement shape is used again.
Microsoft also documents setDisableStatementPooling, where setting the value to false enables statement pooling when used together with a statementPoolingCacheSize greater than zero.
Again, the application code can remain ordinary JDBC code. The behavior is controlled through driver configuration.
Why connection pools usually should not own this feature
It may sound natural for a connection pool to cache prepared statements. After all, the pool already manages connections. Why not statements too?
The problem is that prepared statements are still bound to physical connections.
HikariCP’s documentation explains why it does not provide statement caching at the pool layer. It says prepared statements can only be cached per connection at that layer, and gives the example of 250 commonly executed queries with 20 connections leading to 5,000 query execution plans and cached object graphs.
That is the key point.
A pool-level statement cache can easily multiply state. A driver-level cache can often do a better job because the driver has more direct knowledge of the database protocol, server-prepared statement behavior, invalidation rules, and the exact connection state.
This does not mean driver-level caching is always perfect. It means the driver is usually the more natural place for this optimization.
The connection pool should manage connection lifecycle, health, acquisition, and return. The JDBC driver should manage driver-specific statement behavior.
The common misunderstanding: “the plan is cached”
People often explain statement caching by saying:
“The execution plan is cached.”
That may be partly true in some database/driver combinations, but it is too vague.
There are several layers where reuse can happen.
The driver may cache Java-side statement objects or metadata. It may cache protocol-level information. It may reuse a server-side prepared statement handle. The database may reuse a plan, or it may choose to re-plan depending on parameters, schema changes, statistics, or database-specific rules.
So the safer explanation is:
Statement caching allows the JDBC driver and sometimes the database to reuse statement-related work for repeated SQL.
That is less catchy, but more accurate.
It also prevents a bad assumption: that statement caching guarantees better execution plans. It does not. In some databases, prepared statements can even involve tradeoffs between generic and parameter-sensitive plans. That is a database optimizer topic, not just a JDBC topic.
For most application developers, the practical point is simpler: statement caching reduces repeated setup overhead, not the actual cost of reading, joining, sorting, or locking data.
How it can hurt
Statement caching can hurt when the cache is too large, when SQL is too dynamic, or when server-side resources are limited.
The first risk is memory. PostgreSQL JDBC’s documentation explicitly notes that server-prepared statements consume memory on both client and server, which is why the driver limits them per connection.
The second risk is cache pollution. If your application generates thousands of one-off SQL strings, the cache spends its time remembering statements that will not be reused. That can evict genuinely useful statements.
The third risk is multiplication through pools and replicas. A cache of 256 statements per connection may be reasonable for one connection. Across 40 connections, that becomes a much larger number. Across 20 application instances, it becomes larger again.
This is why the right question is not:
“Should statement caching be enabled?”
The better question is:
“Is my workload repetitive enough, and is my cache sized appropriately for the number of physical connections I actually run?”
Where this matters in modern Java systems
Transparent statement caching was useful in older monoliths, but it becomes more interesting in modern systems because connection counts multiply.
A single service may have a HikariCP pool. Then the service is scaled to 10 pods. Then another service has its own pool. Then another. Every pool has physical connections(except if you use a centralized solution like Open J Proxy). Every physical connection may have its own statement cache.
This is one of the reasons JDBC behavior under the surface matters.
The application code may look clean and simple, but the runtime system contains many hidden layers:
The ORM generates SQL. The pool manages physical connections. The driver manages prepared statement behavior. The database manages parsing, planning, memory, locks, and execution.
Transparent statement caching sits in the middle of those layers. It is not usually visible in business code, but it can still affect latency, memory, database resource usage, and scalability.
This is also why teams should be careful when comparing performance between databases, drivers, or connection-pool setups. One setup may have prepared statement caching enabled. Another may not. One may use server-prepared statements after a threshold. Another may only cache client-side metadata. One may have a small per-connection cache. Another may multiply a large cache across many connections.
If those details are ignored, the benchmark may not be comparing what people think it is comparing.
What to check in a real application
In a real system, I would not start by blindly increasing statement-cache sizes.
I would first check whether the driver supports it, whether it is enabled, and what the defaults are. Oracle, PostgreSQL, MySQL, and SQL Server all expose this area differently, so copying settings from one database to another is a bad idea.
Then I would look at the SQL workload.
If the application repeatedly executes a stable set of prepared statements, caching may help. If the application generates highly dynamic SQL strings, the cache may be less effective. If the application already spends most of its time inside slow query execution, statement caching may only improve the edges.
Finally, I would check the connection count.
A setting that looks harmless per connection may be expensive across many connections, many pods, and many services.
The boring answer is the correct one: measure it.
Look at latency, prepare counts if your database exposes them, server memory, driver metrics where available, and database CPU. Statement caching should reduce repeated prepare overhead. It should not be used as a substitute for query tuning, indexing, schema design, or sensible connection management.
The simple mental model
Transparent statement caching is one of those features that proves JDBC is not as simple as it looks.
The code says:
- prepare
- execute
- close
But underneath, the driver may be saying:
- I have seen this SQL before.
- I still have useful state for it.
- I can avoid doing all the setup work again.
That is the point.
Modern JDBC drivers often provide mechanisms to reuse statement-related work behind the standard JDBC API. Oracle has implicit statement caching. PostgreSQL JDBC has per-connection prepared statement caches and server-prepare thresholds. MySQL Connector/J exposes prepared statement cache settings. Microsoft’s SQL Server JDBC driver supports prepared statement handle caching through statement pooling settings.
The feature is useful, but it is not magic.
It is usually per physical connection. It consumes memory. It depends on stable SQL. It varies by driver. It should be measured.
Used well, transparent statement caching is a quiet performance improvement. Used blindly, it is just another hidden cache in a system that may already have too many hidden caches.
The practical takeaway is simple:
Write stable prepared SQL with bind parameters. Understand your driver’s caching behavior. Size caches with your real connection count in mind. And remember that closing a PreparedStatement does not always mean the driver forgot everything.
Sources used in the article:
- Oracle JDBC — Statement and ResultSet Caching Discusses implicit and explicit statement caching in Oracle JDBC. https://docs.oracle.com/en/database/oracle/oracle-database/19/jjdbc/statement-and-resultset-caching.html
- Oracle JDBC — Statement Caching Discusses implicit statement caching, SQL-string cache keys, and how cached statements are retrieved through normal JDBC calls. https://docs.oracle.com/cd/B13789_01/java.101/b10979/stmtcach.htm
- PostgreSQL JDBC — Connection Parameters / Prepared Statement Cache
Documents
preparedStatementCacheQueries,preparedStatementCacheSizeMiB, and related connection properties. https://jdbc.postgresql.org/documentation/use/ - PostgreSQL JDBC — Server Prepared Statements
Explains
prepareThreshold, server-prepared statements, per-connection caching, and memory implications. https://jdbc.postgresql.org/documentation/server-prepare/ - MySQL Connector/J — Performance Extensions
Documents
cachePrepStmts,prepStmtCacheSize, andprepStmtCacheSqlLimit. https://dev.mysql.com/doc/connector-j/en/connector-j-connp-props-performance-extensions.html - Microsoft JDBC Driver for SQL Server — Setting the Connection Properties
Documents
statementPoolingCacheSizeanddisableStatementPooling. https://learn.microsoft.com/en-us/sql/connect/jdbc/setting-the-connection-properties?view=sql-server-ver17 - Microsoft JDBC Driver for SQL Server —
setDisableStatementPoolingMethod Explains enabling/disabling statement pooling behavior. https://learn.microsoft.com/en-us/sql/connect/jdbc/reference/setdisablestatementpooling-method-sqlserverconnection?view=sql-server-ver17 - HikariCP README — Statement Cache section Explains why HikariCP does not provide statement caching and why driver-level caching is preferred. https://github.com/brettwooldridge/HikariCP#statement-cache
메타데이터
- post_id
- ca1da43c509a
- slug
- transparent-statement-caching-in-modern-jdbc-drivers-ca1da43c509a
- url
- https://medium.com/@rogeriorobetti/transparent-statement-caching-in-modern-jdbc-drivers-ca1da43c509a
- canonical_url
- https://medium.com/@rogeriorobetti/transparent-statement-caching-in-modern-jdbc-drivers-ca1da43c509a
- author_url
- https://medium.com/@rogeriorobetti
- status
- ok
- fetched_at
- 2026-06-17 18:03:35