NHibernate Second-Level Cache with Redis: Architecture Note
Use NHibernate 5.x second-level cache with Redis for shared reference data across app instances, with consistency boundaries, monitoring, and failure-mode guidance.
25 Sept 2025, 23:59 UTC

Problem and takeaway
Multiple application instances repeatedly query the same lookup rows—currency codes, country lists, feature flags—because NHibernate's first-level cache lives only inside a single session. The database absorbs every cache miss even when the data changes once a quarter.
Use NHibernate's second-level (L2) cache with a shared Redis region factory for read-heavy, rarely changed reference data. Treat cached entities as eventually consistent, never cache rows that enforce database-only invariants, and keep the database sized to serve full traffic if Redis is unreachable.
Requirements
Version assumption: NHibernate 5.x with NHibernate.Caches.Redis 2.x. L2 caching fits data that is read frequently and written infrequently. Good candidates:
- Lookup tables and reference dimensions
- Application configuration read at startup or per request
- Read-only lists shared across a server farm
It is not a substitute for indexes, query tuning, or strong transactional consistency. If the cache is empty or unreachable, the database must still meet the cold-start SLA.
Smallest suitable design
Enable L2 caching in the NHibernate configuration and point it at one Redis instance or cluster. Edit hibernate.cfg.xml on each application host. This requires write access to the config file and network access to Redis.
<hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
<session-factory>
<property name="use_second_level_cache">true</property>
<property name="cache.factory_class">NHibernate.Caches.Redis.RedisRegionFactory</property>
<property name="default_expiration">900</property>
<property name="cache.configuration">localhost:6379,ssl=False,abortConnect=False</property>
<property name="cache.serializer">NHibernate.Caches.Redis.Serializers.NewtonJsonSerializer</property>
</session-factory>
</hibernate-configuration>
default_expiration is in seconds; 900 equals 15 minutes. Replace localhost:6379 with your Redis endpoint. abortConnect=False lets the application start when Redis is down and fall back to the database instead of throwing at startup.
Map each cacheable entity explicitly. Region names default to full type names, so set them manually to avoid collisions in a shared cluster:
<class name="Currency" table="currencies"> <cache usage="read-only" region="reference.Currency"/> <id name="Id" column="currency_id"/> <property name="Code" column="code" not-null="true"/> <property name="Name" column="name" not-null="true"/> </class>
Choose the concurrency strategy by data lifecycle:
| Usage | When to use | Consistency expectation |
|---|---|---|
read-only |
Immutable reference data | Stale only until application restart or explicit eviction |
read-write |
Infrequent updates; NHibernate manages eviction on commit | Eventually consistent within TTL or eviction window |
nonstrict-read-write |
Stale reads acceptable; no repeatable-read required | No cache locking; highest risk of stale data |
For entities that change, evict explicitly after writes outside NHibernate's normal flush:
session.Evict(typeof(Currency), currencyId);
session.SessionFactory.EvictQueries("reference.Currency");
Run this code inside the repository or service layer after a successful commit. It requires no special database permissions, only the running application's identity.
Trust and data boundaries
The cache sits outside the database transaction boundary. NHibernate uses cache-aside behavior with write-through on commit: when a transaction commits, updated entities are written to Redis. Another instance can still read the older version until Redis expires the key or NHibernate evicts it.
Never cache entities that participate in invariants enforced only by database constraints, or that require repeatable-read semantics within a business transaction. If two services update the same row through direct SQL, NHibernate has no visibility into that change and will not evict the cached copy.
Operational checks
Enable NHibernate statistics so you can measure cache behavior rather than guess at it:
<property name="generate_statistics">true</property>
Check these indicators:
- Cache hit ratio: target above 90 percent for reference data
- Redis
INFO memoryand keyspace misses to detect eviction pressure - NHibernate DEBUG logs for cache puts and evictions to spot stale-read anomalies
- Database query count during load tests: cached entities should produce fewer than 5 percent of total reads
Run diagnostic Redis commands from a terminal with network access to the cache. Use SCAN instead of KEYS in production because KEYS can block the Redis event loop:
redis-cli -h cache.internal SCAN 0 MATCH "reference.*" COUNT 100 redis-cli -h cache.internal GET "reference.Currency:1"
Expected check: the GET returns JSON-serialized entity data, not NHibernate proxy types. If you see proxy types or circular references, disable proxy caching for that entity or switch to DTO projections.
Failure modes
- Network partition between app and Redis: cache misses degrade to database queries. Acceptable only if the database is sized for cold-start load.
- Stale data from missed eviction: direct SQL updates bypass NHibernate and never trigger cache removal. Mitigate with a short TTL (5–15 minutes) and explicit eviction after any out-of-band write.
- Serialization exceptions: cached proxies can fail deserialization on another instance. Disable lazy proxies for cached entities or cache DTOs instead of entities.
Conditions that change the design
- Write-heavy entities: disable second-level cache; use query cache only for immutable result sets.
- Multi-region deployment with high cross-region latency: prefer local in-memory cache with an invalidation bus over a single distributed cache.
- Upgrade to NHibernate 6.x: evaluate the built-in StackExchangeRedis provider and changed cache concurrency strategies before adopting.
Limitations and verification
Second-level cache does not replace proper indexing, and cold-cache performance must meet SLAs. Entities with lazy-loaded collections cached in read-write mode can produce stale collection snapshots; cache root entities only and use batch fetching for collections.
Practical verification steps:
- Deploy a test entity with
usage="read-write", enable statistics, and run two application instances. Update through one instance and confirm the other sees the change within the TTL window. - Stop Redis and confirm the application falls back to the database without exceptions, accepting only increased latency.
- Inspect serialized entries in Redis to confirm no proxy types are stored.
- Review NHibernate 5.4+ release notes and the Redis cache provider changelog for breaking changes before upgrading.
Rollback
Because this change modifies application configuration, rollback means reverting hibernate.cfg.xml and entity mappings to disable use_second_level_cache or remove cache.factory_class, then restarting the application. No database schema change is required.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.