Skip to main content

Performance

Performance — performance optimization, benchmarking, and profiling in Java applications

Master Hibernate 7 Connection Pooling with HikariCP: The Definitive Performance Guide

Are you struggling with sluggish database response times or "Connection is closed" exceptions in your Java logs? In modern enterprise applications, your database connection pool is the heart of your infrastructure. If that heart beats too slowly, your entire system suffers. In this guide, we explore how to integrate Hibernate 7 with HikariCP—the "zero-overhead" connection pool—to achieve maximum throughput and reliability. The Problem: The Latency of Connection Handshakes Database connections are heavy. When Hibernate needs to execute a query without a pool, it must perform a series of time-consuming steps: Open a network socket to the DB server. Complete a TCP/IP handshake. Negotiate SSL/TLS security. Authenticate the database user. Under high load, these milliseconds accumulate, leading to massive latency spikes. Without a pool, your application spends more time "connecting" than actually "querying." The Agitation: Why "Old School" Pools are Falling Behind For years, developers relied on pools like c3p0 or DBCP. While reliable, these libraries were built for an era of lower concurrency. They often suffer from: Internal Locking: Threads frequently block each other just to "borrow" a connection. Complexity: Dozens of confusing parameters that lead to misconfiguration. Size: Heavyweight codebases that increase your application's memory footprint. The Solution: Hibernate 7 + HikariCP HikariCP is widely recognized as the fastest connection pool available for the JVM. It is built on highly optimized, lock-free data structures and micro-benchmarked to ensure near-zero overhead.

Mastering Hibernate 6 L2 Caching with Ehcache 3: The Definitive Guide

Hibernate 6 introduced a major architectural shift. By moving to the Jakarta EE namespace and embracing the JCache (JSR-107) standard, it changed how we interact with caching providers. If you are using Hibernate 6, the legacy hibernate-ehcache dependency is dead. To achieve high-performance data access today, you need the modern JCache bridge. The Problem: Database Bottlenecks in Hibernate 6 Even with the performance improvements in Hibernate 6’s new SQM (Semantic Query Model), database latency remains the primary bottleneck for scaling. Without a Second Level (L2) Cache, every time a new Session (EntityManager) is opened—even for the same user—Hibernate is forced to hit the database for data that hasn't changed. This results in redundant SQL SELECT statements, higher DB CPU usage, and increased costs in cloud environments where you pay for IOPS and database instance sizing. The Solution: Hibernate 6 + Ehcache 3 (JSR-107) The modern solution for Hibernate 6 is to use Ehcache 3 as a JCache provider. This allows Hibernate to offload entity and collection state to memory, sharing it across all sessions in the SessionFactory. Prerequisites Java 11+: Hibernate 6 requires a minimum of Java 11 (it is Hibernate 7 that raises the baseline to Java 17). Jakarta Persistence 3.x: The modern jakarta.persistence namespace. Step 1: Hibernate 6 Dependencies In Hibernate 6, you must use the hibernate-jcache module. Crucially, your Ehcache dependency must include the jakarta classifier to avoid namespace conflicts.

Master Hibernate 7 Ehcache 3 Configuration: High-Performance Caching with Jakarta Persistence

Are you struggling with database performance in your modern Java applications? As systems scale, database bottlenecks remain the primary cause of latency. While Hibernate 7 introduces massive improvements in query generation and Jakarta Persistence compatibility, the secret to true high-concurrency performance lies in the Second-Level (L2) Cache. In this guide, we will configure Ehcache 3 — the industry standard for JVM-level caching — as the L2 cache provider for Hibernate 7, using the modern JCache (JSR-107) bridge.

Hibernate 7 Second-Level Cache: When to Turn It On, How to Configure Ehcache 3, and the Three Ways It Will Stale Your Data

The question that matters before enabling L2 cache is not "can I make it faster" but "can I tolerate stale data, and for how long". The cache sits between Hibernate and the database, serving data that may have been written by a different JVM process, a batch job, a DBA running a script, or another application instance. Every one of those paths can invalidate the cache without Hibernate knowing. If your answer to the staleness question is "no, I cannot tolerate any staleness" — skip L2 entirely. If the answer is "yes, but only for these entities and within these bounds" — read on. This post covers what L2 actually stores, how to configure Ehcache 3 as a JCache provider, what changed between Hibernate 6 and 7, the three specific staleness modes that catch production teams off guard, and why the query cache is almost always the wrong additional layer.

The Hibernate First-Level Cache Explained (It’s Not What You Think It Is)

Most developers who have used Hibernate for any length of time know the first-level cache exists. Ask them to describe it and you'll hear something like: "It's a cache Hibernate uses so it doesn't hit the database twice for the same row." That's technically correct, but it misses almost everything that matters. The first-level cache is not a feature you enable, a setting you tune, or an optional layer you bolt on for performance. It is the persistence context itself — always-on, transaction-scoped by default in Spring, and the thing that makes dirty checking, identity guarantees, and cascade operations possible. If you have ever used em.find(), you have used it. If you have ever hit an OutOfMemoryError in a batch job that loaded 200,000 entities, the first-level cache is why. This post is a deep look at the mechanics: what the persistence context stores, when it is consulted, how EntityKey works, and the failure modes that catch experienced developers off guard.

Mastering Stored Procedures with Hibernate 7: A Deep Dive for High-Performance Java Apps

If you’ve ever written nested loops in Java just to process thousands of records—only to watch latency skyrocket—you’re not alone. This application–database “chattiness” is a silent performance killer that creeps into enterprise systems as they scale. Every time your application fetches a row, transforms it in memory, and sends it back to the server, you incur the cumulative overhead of network round-trips, intensive object-relational mapping (ORM) overhead, and heavy JVM garbage collection cycles. For a few dozen records, this is negligible; for a few million, it is a catastrophic bottleneck that can bring a production environment to its knees. The solution? Shift data‑intensive logic into the database layer and invoke it through Hibernate 7. In this guide, you’ll learn when and how to use stored procedures safely, portably, and efficiently. Why Use Stored Procedures in Hibernate 7? Hibernate 7 continues to improve support for native database features while aligning fully with Jakarta Persistence 3.2. Stored procedures are not a silver bullet, but in the right scenarios they offer tangible benefits: Reduce Network Latency: Execute complex logic in a single round-trip instead of hundreds of individual queries. Centralize Logic: Keep data-heavy calculations close to the data to avoid serialization overhead. Security: Expose only procedures instead of granting direct table access. Traffic Optimization: Offload bulk relational work to the database engine, which is optimized for it. Note: Hibernate also provides the native ProcedureCall API for finer-grained control. For most portable applications, prefer the JPA-standard StoredProcedureQuery. The PAS Framework: Problem, Agitation, Solution The Problem Your application must calculate a year-end bonus based on multiple tables: Sales, Attendance, Tenure, and departmental performance.

BLOB and CLOB in Hibernate 7: Streaming vs Eager, and the OOM You Didn’t See Coming

A list endpoint returned a page of 100 products. Simple enough — a cheap SELECT should be fast. But the page timed out, the heap spiked to 4 GB, and the GC ran continuously. The cause: the Product entity had an @Lob byte[] thumbnail field mapped with default eager fetching. Each of the 100 products loaded its thumbnail — averaging 40 MB each — all at once, into the heap. 100 rows × 40 MB = 4 GB from a list query that didn't display thumbnails. This is the OOM you don't see coming because the entity mapping looks harmless. This post covers the difference between byte[] (always eager), Blob with bytecode enhancement (genuinely lazy), and streaming (no heap allocation at all) — with approximate memory numbers for each. If you are building modern Java applications, handling BLOB and CLOB with Hibernate 7 is a skill you cannot ignore. In this guide, we will dive deep into how to efficiently map, persist, and retrieve binary and character data using the latest Hibernate standards aligned with Jakarta Persistence 3.2+. The Problem: The "Out of Memory" Nightmare Storing small strings like usernames or emails is easy. But what happens when your data grows to megabytes? Traditional mapping techniques often try to load the entire object into the JVM's memory. Imagine a scenario where 100 concurrent users try to download a 50MB PDF. If your application is configured to load the entire file into a byte[], your server will attempt to allocate 5GB of RAM instantly. In most environments, this leads to the dreaded: java.lang.OutOfMemoryError: Java heap space This crashes your service and disrupts all users.

@OneToMany Done Right: Set vs List, Bidirectional Sync, and the MultipleBagFetchException Trap

Hibernate has a strong preference between Set<Child> and List<Child> in a @OneToMany mapping. If you use List and try to JOIN FETCH two collections simultaneously, you get MultipleBagFetchException. If you use List and remove one child, Hibernate deletes all children and reinserts the remainder. If you use Set, a single-child removal fires one targeted DELETE. The performance difference on a parent with 500 children is the difference between 1 SQL statement and 501. This post covers Set vs List semantics, why the bidirectional sync helper method matters and what breaks without it, and the MultipleBagFetchException with its three fixes. ⚠️ Important: All persistence operations must occur within an active transaction. Accessing lazy collections or proxies outside a transaction boundary will result in the dreaded LazyInitializationException. Always ensure your Service layer is marked with @Transactional or manually manage your transaction lifecycle. The Problem: Data Fragmentation and Manual Syncing Imagine you are building an e-commerce platform. A single Customer can place multiple Orders. In a raw SQL world, you’d have to manually manage foreign keys, write complex joins, and ensure that when a customer is deleted, their orphaned orders don't break your database integrity. Manually mapping these relationships in Java code leads to "Boilerplate Hell"—hundreds of lines of code spent manually updating IDs, checking for nulls, and keeping two separate objects in sync. This manual labor is error-prone and often leads to data inconsistency between your application memory and the actual database state.

Mastering Hibernate 7 with Spring Boot 4: The Next-Gen Configuration & Performance Guide

Are you ready to move your data layer into the future? Configuring Hibernate 7 with Spring Boot 4 represents a significant milestone in the Java ecosystem. Spring Boot 4 (released November 2025) aligns with Jakarta EE 11 APIs, and Hibernate 7 introduces native JSON support and refined type systems — so developers are encountering new challenges, from namespace migrations to modern JVM targets (the baseline is Java 17, with Java 21 or 25 recommended for virtual threads). If your application feels stuck in the past, this guide is your roadmap to the cutting edge. In this deep-dive guide, we’ll explore the high-performance world of Spring Boot 4. We will cover the mandatory shifts in Jakarta Persistence 3.2, advanced performance tuning for virtual threads, and how to leverage Hibernate 7’s modern features to ensure your application on ankurm.com is ready for the evolving landscape of 2026 and beyond. The Problem: Legacy Debt in a Modern World Many applications are still tethered to the legacy javax.* namespace or older Hibernate versions that lack the efficiency of modern JVM features like Project Loom (Virtual Threads). Using outdated configurations leads to "Namespace Collision" errors and missed opportunities for the significant memory and performance optimizations anticipated in modern runtime environments. The Agitation: The Risk of Stagnation As Java moves toward the anticipated widespread adoption of Java 21 and the eventual standard of Java 25, staying on Spring Boot 2 or 3 becomes an increasing security and performance liability. Hibernate 7 introduces breaking changes in how it handles specific database dialects and type mappings. Failing to plan your migration now could lead to technical debt, incompatible libraries, and a data layer that cannot fully leverage modern hardware. The Solution: Harnessing Spring Boot 4 & Hibernate 7 Spring Boot 4 is built for speed, native compilation (GraalVM), and seamless integration with Hibernate 7. By migrating to the jakarta.persistence namespace and leveraging the "Unified Type System" introduced in Hibernate 7, we can build data layers that are significantly leaner and more performant.

Lazy Loading in Hibernate 7: Three Fetch Strategies, Benchmarked, with the One Most Tutorials Get Wrong

Switching from EAGER to LAZY on a @ManyToOne association made one of our REST endpoints 4x slower. That's the opposite of what every Hibernate tutorial promises, and understanding why it happened is more useful than any fetch-type cheatsheet. The endpoint served an order detail page. The Order had a @ManyToOne Customer, which was previously EAGER. Response time was ~80ms. We switched to LAZY (correctly, as a "best practice") and response time jumped to ~320ms. The reason: the EAGER mapping had been loading Customer in the same query as Order via a JOIN. LAZY replaced that with a second SELECT — and the second SELECT ran after the session had been handed to Jackson for serialisation, which triggered OSIV to keep the connection open, added latency from the extra round-trip, and eventually started failing under load when connection pool slots ran out waiting for the serialisation thread to finish. LAZY was still the right answer. The fix was a proper fetch strategy. But "switch everything to LAZY" without understanding the four fetch options and when each one earns its keep is how you trade one performance problem for a different one.