Overview
The default answer for a distributed cache in any greenfield discussion is Redis. That default is usually correct. But it rests on an implicit assumption: that the value you are caching is a serialised blob that lives outside your JVM, retrieved over a TCP socket. For the subset of financial workloads where the cached object is actively computed against — a risk engine iterating a portfolio, a payment router evaluating limit structures, a fraud model hydrating feature vectors — the network round trip to Redis becomes the bottleneck, not the database query you were trying to avoid.
Hazelcast IMDG (In-Memory Data Grid) places the data inside the application process or, in client-server mode, inside a co-located cluster that your code reaches via a smart client that knows which node owns each partition. The canonical Hazelcast embed eliminates the serialisation/network/deserialisation cycle entirely for local reads. The trade-off is that your data grid is now part of your application process: JVM heap pressure, GC pauses, and ClassLoader isolation all become caching concerns.
This article covers when that trade-off is worth it, how to configure it correctly, and what breaks in production if you get the topology wrong.
Hazelcast has rebranded and consolidated products over the years. What this article calls IMDG is the core distributed data structure layer available in the open-source hazelcast JAR (formerly hazelcast-imdg). Hazelcast Jet is the stream-processing engine built on top; Hazelcast Platform is the commercial bundle. Unless you need distributed pipelines, the open-source JAR is sufficient for the caching use cases covered here.
Topology modes
Hazelcast supports two deployment topologies. The choice determines where partitions live and how the application accesses them.
| Topology | Data location | Read path | When to use |
|---|---|---|---|
| Embedded | JVM heap of every app pod | Local partition: heap read. Remote partition: one network hop to owning pod. | Compute-intensive workloads, small data volume, homogeneous app fleet |
| Client-server | Dedicated HZ cluster pods | Always one hop to the owning HZ pod (smart client skips extra hops) | Large working set, heterogeneous consumers, need to scale cache independently |
When app pods restart during a rolling deploy, their embedded HZ members leave the cluster. Data partitions migrate to surviving members. During migration, reads for the affected partition block briefly. With 3 replicas and backup-count=1, you have a short migration window on every deploy — not a problem for cache data, but worth knowing before you embed anything you cannot afford to re-read from the database.
Partition awareness
Hazelcast divides its keyspace into 271 partitions (the default, tunable at cluster creation). Every key is assigned to exactly one primary partition via a consistent-hash of its serialised form, with one backup partition elsewhere in the cluster. The smart client (in client-server mode) and the embedded member both maintain a local partition table that maps partition ID to cluster member address.
The consequence: a map.get(key) from a smart client resolves the owning member locally and sends exactly one network message. There is no proxy or router in the data path. This is what separates Hazelcast's read latency from Redis Cluster at scale — Redis Cluster also routes efficiently, but it adds a protocol-level MOVED/ASK redirect cycle for misrouted commands; Hazelcast's smart client is always correct on the first send.
// Partition-aware key design: co-locate related data on same partition
// by wrapping the key with PartitionAware
public class AccountKey implements PartitionAware<String>, Serializable {
private final String accountId;
private final String txnId;
// All keys for the same account land on the same partition.
// EntryProcessor.executeOnKey() then avoids any remote call.
@Override
public String getPartitionKey() {
return accountId;
}
}
// EntryProcessor runs on the owning partition member: no serialisation round-trip
BigDecimal newBalance = (BigDecimal) accountMap.executeOnKey(
new AccountKey(acctId, txnId),
new DebitEntryProcessor(amount)
);
The PartitionAware key is the most important performance primitive in Hazelcast for financial workloads. By co-locating all account-level keys on the same partition, an EntryProcessor that debits a balance, updates a limit counter, and appends an audit entry can do all three operations in a single partition-local transaction — no distributed coordination, no network round trips within the operation.
Distributed data structures
Hazelcast provides a richer type system than Redis. The structures most useful in financial integration workloads:
| Structure | API | Financial use case |
|---|---|---|
| IMap | ConcurrentMap-compatible | Reference data cache, limit cache, session state |
| MultiMap | One key → multiple values | Account → active transaction IDs, customer → cards |
| IQueue | Distributed blocking queue | Work queue for async payment processing |
| IAtomicLong | Distributed atomic counter | Sequence number generator (idempotency keys) |
| FencedLock | CP-subsystem distributed lock | Distributed mutex on settlement window |
| CPMap | Linearizable map (CP subsystem) | Configuration that must be consistent across all nodes |
IMap is AP — it uses eventual consistency with configurable backup-count. FencedLock and CPMap belong to the CP subsystem, which runs a Raft group and guarantees linearizability at the cost of requiring an odd-number quorum (≥3 CP members). Do not mix AP and CP expectations on the same data: IMap cannot guarantee mutual exclusion; FencedLock cannot be sharded across 271 partitions.
Near-cache
Near-cache adds a local in-process cache in front of the remote partition. On a read, if the key is in near-cache, the response is a heap lookup — no serialisation, no network. This is the pattern that takes client-server read latency below 50 μs on hot keys.
Near-cache introduces stale reads. By default, invalidation is pushed from the owning partition on every write. This means all clients holding a near-cached value receive an invalidation message within milliseconds of an update. In practice, for reference data that updates once per minute or slower (exchange rates, product configurations, limit matrices), near-cache is safe and the latency benefit is dramatic. For data that changes per-transaction, near-cache is wrong — the invalidation traffic becomes its own bottleneck.
Deploying on Kubernetes
-
Add the Kubernetes discovery plugin
The
hazelcast-kubernetesplugin uses the Kubernetes API to discover cluster members. It requires a ServiceAccount withgetandliston Endpoints and Pods. Create the ClusterRole once per namespace; bind it to the pod's ServiceAccount. -
Choose embedded or client-server
For embedded: include
hazelcastas a compile dependency and Hazelcast boots with the Spring application context. For client-server: deploy a separate StatefulSet for the HZ cluster and addhazelcast-clientas a dependency instead — nothazelcast. Mixinghazelcastandhazelcast-clientin the same pod is valid for development; avoid it in production. -
Configure member discovery
Set
kubernetesdiscovery in the Hazelcast YAML. Point it at the headless Service used by the StatefulSet (client-server) or at the app Deployment's label selector (embedded). The headless Service is simpler and more reliable than namespace-scoped pod listing. -
Set partition backup count
For 3-node clusters,
backup-count: 1means every partition has one sync backup. Withasync-backup-count: 1you get two copies (primary + async backup) without synchronous replication latency. For financial data where loss is unacceptable, usebackup-count: 1(synchronous) and ensure you have at least 3 members at all times. -
Configure graceful shutdown
Add
SIGTERMhandling so Hazelcast migrates its partitions before the pod terminates. Sethazelcast.shutdownhook.policy=GRACEFULand setterminationGracePeriodSecondsin the Deployment to at least 60 seconds. Rolling deploys without graceful shutdown trigger partition migration under traffic — avoidable latency spikes. -
Size JVM heap conservatively
In embedded mode, Hazelcast competes with the application for heap. Allocate at least 40% of the container memory limit to non-Hazelcast objects (thread stacks, code cache, application working set). Hazelcast stores serialised bytes on heap by default; if data volume is large, consider HD Memory (Hazelcast Platform only) or client-server to keep the app heap clean.
Configuration
hazelcast:
cluster-name: saib-payment-grid
network:
join:
multicast:
enabled: false
kubernetes:
enabled: true
namespace: payments
service-name: payment-svc-hazelcast # headless Service
map:
# Limit reference data: stable, high-read, low-cardinality
limit-matrix:
backup-count: 1
async-backup-count: 0
time-to-live-seconds: 300
max-idle-seconds: 0
eviction:
eviction-policy: LRU
max-size-policy: PER_NODE
size: 50000
near-cache:
time-to-live-seconds: 60
max-idle-seconds: 30
invalidate-on-change: true
eviction:
eviction-policy: LFU
max-size-policy: ENTRY_COUNT
size: 5000
# Payment session: per-transaction, short TTL, no near-cache
payment-session:
backup-count: 1
time-to-live-seconds: 120
eviction:
eviction-policy: LRU
max-size-policy: PER_NODE
size: 200000
cp-subsystem:
cp-member-count: 3 # Raft quorum for FencedLock / CPMap
group-size: 3
properties:
hazelcast.shutdownhook.policy: GRACEFUL
hazelcast.graceful.shutdown.max.wait: 60
hazelcast.logging.type: slf4j
Spring Boot integration
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public HazelcastInstance hazelcastInstance() {
Config config = new YamlConfigBuilder(
getClass().getResourceAsStream("/hazelcast.yaml")).build();
return Hazelcast.newHazelcastInstance(config);
}
@Bean
public CacheManager cacheManager(HazelcastInstance hz) {
// Wraps IMap as a Spring Cache — @Cacheable works transparently
return new HazelcastCacheManager(hz);
}
}
// Usage: cache-aside with TTL inherited from hazelcast.yaml map config
@Cacheable(value = "limit-matrix", key = "#productCode + ':' + #currency")
public LimitMatrix getLimitMatrix(String productCode, String currency) {
return limitRepository.findByProductAndCurrency(productCode, currency);
}
Consistency model
IMap is AP in CAP terms: it prioritises availability over strict consistency. In practice this means:
- Writes are synchronous to the primary partition and asynchronous to backups when
async-backup-count > 0. A primary failure before backup sync completes loses the write. - Reads from the primary partition are always current. Reads from a replica (if you configure read-from-backup) may be stale by the replication lag.
- EntryProcessor is atomic on the partition. It reads, modifies, and writes in a single partition-local operation with no other thread interleaving. This is the right tool for read-modify-write on IMap without external locking.
The common mistake: reading a value, modifying it in the application, and writing it back. Between get and put, another node can modify the same key. The result is a lost update. Use EntryProcessor, map.replace(key, expectedValue, newValue), or the CP subsystem's CPMap when atomicity matters. This is the class of bug that produces incorrect limit consumption in a payment router under concurrent load.
// Safe: runs atomically on the partition that owns accountId
public class DebitEntryProcessor
implements EntryProcessor<AccountKey, AccountState, DebitResult> {
private final BigDecimal amount;
@Override
public DebitResult process(Map.Entry<AccountKey, AccountState> entry) {
AccountState state = entry.getValue();
if (state == null) return DebitResult.NOT_FOUND;
if (state.getAvailableBalance().compareTo(amount) < 0)
return DebitResult.INSUFFICIENT_FUNDS;
state.debit(amount);
entry.setValue(state); // writes back atomically
return DebitResult.SUCCESS;
}
}
Observability
Hazelcast 5.x exposes metrics via Micrometer when hazelcast-spring is on the classpath. Key metrics to alert on in production:
- hazelcast.map.get.hits / (hits + misses). Near-cache hit rate. Alert if near-cache hit rate for
limit-matrixdrops below 80% — means invalidation is too aggressive or TTL is too short. - hazelcast.map.put.count. Write rate per IMap. Useful to detect unexpected write bursts that trigger invalidation floods.
- hazelcast.partition.migration.active. Alert if this stays >0 for more than 30 seconds — indicates a member left and partitions are migrating, which degrades read latency.
- hazelcast.executor.queue.size for the partition executor. A queue buildup means partition threads are saturated — check for slow EntryProcessor implementations or lock contention.
- JVM heap. In embedded mode, Hazelcast lives on the same heap as the application. Watch
jvm.memory.usedand alert at 75% of-Xmx, not the Kubernetes memory limit, to leave room for GC overhead.
groups:
- name: hazelcast
rules:
- alert: HazelcastNearCacheHitRateLow
expr: |
rate(hazelcast_map_get_hits_total{map="limit-matrix"}[5m])
/ rate(hazelcast_map_gets_total{map="limit-matrix"}[5m]) < 0.8
for: 10m
labels:
severity: warning
- alert: HazelcastPartitionMigrating
expr: hazelcast_partition_migration_active > 0
for: 30s
labels:
severity: critical
When Hazelcast wins
Three situations in KSA financial services where Hazelcast consistently outperforms a Redis-based architecture:
1. Risk computation with large object graphs. A risk engine that loads a portfolio of 500 instruments, computes VaR using Monte Carlo, and returns a scalar might read 500 Redis keys, deserialise each object, build the graph in Java, compute, and discard. With embedded Hazelcast, the portfolio objects are already in the JVM heap. The computation runs on the data in place. At SAIB scale — hundreds of portfolios evaluated per minute at batch cut-off — the serialisation/network overhead in Redis is a measurable share of processing time.
2. Stateful payment routing with limit enforcement. A payment router that must check 4 limit counters (daily, weekly, monthly, per-beneficiary) before authorising a payment can do so with a single executeOnKey call if the counters are co-located on the same Hazelcast partition via PartitionAware. The atomic EntryProcessor gives linearizable read-modify-write semantics without distributed locking. The same pattern in Redis requires a Lua script with 4 HINCRBY calls and a transaction, and the network round trip is unavoidable.
3. Rate-limiting and velocity checks. Fraud detection velocity checks (N transactions per account in 5 minutes) require an in-process sliding window. With embedded Hazelcast, the window lives in the JVM heap and the check is a heap read. With Redis, it is a ZRANGE + ZADD + ZREMRANGEBYSCORE sequence — three round trips or one Lua script. Sub-10 ms fraud pre-screening is achievable with Hazelcast; it requires real investment in Redis pipeline design to match.
| Use case | Hazelcast (embedded) | Redis Cluster | Winner |
|---|---|---|---|
| Reference data cache, large objects | Heap read + near-cache | GET + deserialise | Hazelcast |
| Atomic read-modify-write | EntryProcessor, partition-local | Lua script, network | Hazelcast |
| Shared session state across heterogeneous services | Requires common data model | Any client reads any key | Redis |
| Very large working set (>50 GB) | JVM heap pressure | External, no GC impact | Redis |
| Pub/sub or Streams | ITopic (limited) | Pub/Sub, Streams, rich | Redis |
| Multi-language consumers | Hazelcast client for each language | Native clients everywhere | Redis |
Production pitfalls
A network partition can divide a Hazelcast cluster into two groups, each believing it is the primary for the same partitions. Both sides continue to accept writes; when the partition heals, Hazelcast must merge the two versions. Configure merge-policy explicitly — the default is PutIfAbsentMergePolicy, which silently drops one side's writes. For limit counters, use PassThroughMergePolicy and a compensating reconciliation job. In Kubernetes, split-brain is most likely during node drain events with long-lived TCP connections; mitigate by keeping cluster size at 3 or 5 (odd quorum).
A rolling deploy upgrades half the pods to a new version while old pods still run. If the new version adds a field to a serialised class that Hazelcast stores on the heap, the old pods will fail to deserialise it when they receive a partition backup. Use IdentifiedDataSerializable or Portable serialisation with explicit version fields — not Java's default serialisation, which does not support forward compatibility.
IMap with an EntryListener is not a reliable queue. Listeners are best-effort and fire asynchronously; they do not guarantee exactly-once delivery and are not persisted across cluster restarts. For reliable work distribution, use IQueue or a proper broker (Kafka, IBM MQ). This distinction has caused several missed payment notifications in banking deployments that tried to use map listeners as event triggers.