Designing NHibernate Second‑Level Cache for Read‑Heavy Workloads
Learn how to add NHibernate’s second‑level cache to cut database reads for reference data, with configuration, checks, and failure‑mode guidance.
18 Aug 2026, 13:59 UTC

Requirements
The application performs many reads of reference data (e.g., product catalog, lookup tables) that change infrequently. Each read currently hits the database, causing unnecessary latency and load. The goal is to reduce database round‑trips for these reads while accepting a brief window of stale data.
Smallest Suitable Design
Enable NHibernate’s second‑level cache with an in‑process provider for a single‑node deployment, marking only the reference entities as read‑only. This adds a cache layer that lives alongside the session factory and is shared by all sessions.
Configuration (XML)
<hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
<session-factory>
<property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property>
<property name="connection.driver_class">NHibernate.Driver.SqlClientDriver</property>
<property name="connection.connection_string">Server=.;Database=AppDb;Trusted_Connection=True;</property>
<property name="dialect">NHibernate.Dialect.MsSql2012Dialect</property>
<property name="show_sql">true</property>
<property name="use_second_level_cache">true</property>
<property name="cache.provider_class">NHibernate.Caches.SysCache2.SysCacheProvider, NHibernate.Caches.SysCache2</property>
<property name="cache.use_query_cache">false</property>
<!-- region definition for reference data -->
<property name="cache.regions">ReferenceData</property>
</session-factory>
</hibernate-configuration>
Mapping (Fluent NHibernate)
public class ProductMap : ClassMap<Product>
{
public ProductMap()
{
Table("Products");
Id(x => x.Id);
Map(x => x.Name);
Map(x => x.Price);
Cache.ReadOnly().Region("ReferenceData");
}
}
Trust/Data Boundaries
The second‑level cache stores only entity state that is considered immutable for the duration of the cache region’s lifetime. Trust is placed in the cache provider to keep data consistent within a single process boundary. Any data that can be modified outside NHibernate (e.g., via direct SQL, another service, or a batch job) crosses the trust boundary and can invalidate the cache.
Operational Checks
- Hit/miss ratio: After enabling
show_sql=trueanduse_second_level_cache=true, run a workload that repeatedly loads the same entity. UsesessionFactory.Statisticsto readEntityFetchCount(cache hits) andEntityLoadCount(database loads). A rising hit count with stable load count indicates effective caching. - Memory usage: Monitor the process’s private bytes or the cache provider’s specific metrics (e.g., SysCache2 performance counters) to ensure the region does not cause excessive pressure.
- Expiration verification: Temporarily change a row directly in the database, then reload the entity through NHibernate. The cached stale value should be returned until the region’s expiration period elapses or you manually evict it.
Example Diagnostic Commands (run in a developer console)
# Build and run the console app (requires .NET 6+ SDK)
dotnet run --project NhibernateCacheDemo.csproj
# While the app is running, open another console and query NHibernate statistics:
# (Assume you have exposed a static method that returns sessionFactory.Statistics)
var stats = NhibernateCacheDemo.GetFactoryStatistics();
Console.WriteLine($"Entity loads: {stats.EntityLoadCount}");
Console.WriteLine($"Entity fetches: {stats.EntityFetchCount}");
Console.WriteLine($"Hit ratio: {(double)stats.EntityFetchCount / (stats.EntityFetchCount + stats.EntityLoadCount):P2}");
No special permissions are required beyond the ability to run the compiled executable and to connect to the configured SQL Server instance.
Failure Modes
- Cache incoherence: If data is updated outside NHibernate (e.g., a stored procedure or another application) without triggering a cache invalidation, subsequent NHibernate reads return stale values until the region expires or is manually cleared.
- Distributed inconsistency: Using an in‑process provider (SysCache2) in a web farm results in each node holding its own copy of the cache. Updates on one node are not visible to others, leading to divergent data across servers.
- Write‑heavy overhead: Every session flush incurs extra work to update or lock cache entries. In workloads with frequent writes, the cache can actually increase latency and memory consumption.
Conditions That Would Change the Design
- If the reference data changes more often than the cache expiration interval, a
read‑writeornonstrict‑read‑writestrategy would be needed, adding complexity and still risking stale reads under high concurrency. - When the application runs across multiple servers, replace the in‑process provider with a distributed cache (e.g., Redis via
NHibernate.Caches.Redis) to keep a single source of truth. - If strong consistency is required (no tolerance for stale reads), disable the second‑level cache and rely on database indexing or query optimization instead.
- Should profiling reveal that the cache region consumes a disproportionate amount of memory, consider splitting entities into finer‑grained regions or reducing the cached property set.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.