Auto-registering Hazelcast Compact serialization in Spring Boot
Hazelcast Toolkit series: Part 2.
Auto-registering Hazelcast Compact serialization in Spring Boot
Hazelcast Toolkit series: Part 2.

If you use Hazelcast from a Spring Boot service, serialization setup is one of those pieces that often starts small and quietly turns into infrastructure code.
At first, you only need to put a value object into an IMap. Then you want Hazelcast Compact serialization because it is schema-based, efficient, and does not require your domain objects to implement Serializable. Then you need to register a few Compact classes. Then one class needs an explicit serializer. Then a second module needs the same registration logic. Before long, your application has a custom ClientConfig bean whose real job is mostly bookkeeping.
This is exactly the kind of setup Hazelcast Toolkit tries to remove.
The goal is simple:
@HzCompact
public class UserProfile {
private String userId;
private String nickname;
}
Configure the package once:
hazelcast:
toolkit:
compact:
base-package: com.example.app.model
And let the toolkit scan, validate, and register the Compact serialization configuration when the Hazelcast client is created.
Why Compact serialization matters
Hazelcast Compact serialization is a better default than Java serialization for modern distributed applications:
- it is schema-based;
- it is more efficient than Java serialization;
- it does not require classes to implement java.io.Serializable;
- it is a better fit for cross-language data models;
- it lets you choose between reflective registration and explicit serializers.
That last point is important. For simple value classes, reflective registration is usually enough. For data that needs stable field names, custom enum encoding, versioning rules, or cross-language compatibility, you still want an explicit CompactSerializer.
So the real problem is not whether Compact serialization is useful. It is how much manual Spring Boot wiring you need before your application can use it cleanly.
The usual manual setup
Without additional tooling, Compact registration tends to end up inside Hazelcast client configuration:
@Bean
public HazelcastInstance hazelcastClient() {
ClientConfig config = new ClientConfig();
config.getNetworkConfig()
.addAddress("127.0.0.1:5701");
config.getSerializationConfig()
.getCompactSerializationConfig()
.addClass(UserProfile.class);
config.getSerializationConfig()
.getCompactSerializationConfig()
.addSerializer(new OrderEntryCompactSerializer());
return HazelcastClient.newHazelcastClient(config);
}
This works, but it has a few drawbacks:
- every new Compact type requires a central config edit;
- registration logic is far away from the type it describes;
- explicit serializers can be mismatched with the wrong domain class;
- shared modules usually need conventions that are not visible in code;
- tests need to repeat or inspect the same low-level ClientConfig setup.
It is not hard code. It is worse: it is easy code that has to be remembered forever.
The Hazelcast Toolkit approach
Hazelcast Toolkit makes Compact registration annotation-driven.
There are two modes behind one annotation:
- Reflective registration for simple value classes.
- Explicit serializer registration when you need full control.
The application only declares where Compact-enabled classes live:
hazelcast:
client:
cluster-name: dev
network:
cluster-members:
- 127.0.0.1:5701
toolkit:
compact:
base-package: com.example.app.model
At startup, the toolkit scans com.example.app.model, finds classes annotated with ***@HzCompact, and applies them to Hazelcast’s CompactSerializationConfig***.
The flow looks like this:

The important part is that the registration flow stays small: classes declare intent, the scanner finds them, and the client configuration receives either reflective classes or explicit serializers.
Add the starter
For a Spring Boot 3 application, use the toolkit starter:
Gradle:
implementation 'io.github.javaquasar:hazelcast-toolkit-spring-boot3:<version>'
Maven:
<dependency>
<groupId>io.github.javaquasar</groupId>
<artifactId>hazelcast-toolkit-spring-boot3</artifactId>
<version>${version}</version>
</dependency>
Then configure the Hazelcast client and the Compact scan package:
spring:
application:
name: profile-service
hazelcast:
client:
cluster-name: dev
network:
cluster-members:
- 127.0.0.1:5701
toolkit:
compact:
base-package: com.example.profile.model
The base-package value should point to the package that contains your ***@HzCompact*** classes. In larger applications, choose a package that is specific enough to avoid scanning unrelated code, but broad enough to cover shared cache DTOs.
Mode 1: zero-config reflective registration
The smallest possible registration is just the annotation:
package com.example.profile.model;
import io.github.javaquasar.hazelcast.toolkit.annotation.HzCompact;
import java.math.BigDecimal;
@HzCompact
public class UserProfile {
private String userId;
private String nickname;
private BigDecimal balance;
public UserProfile() {
}
public UserProfile(String userId, String nickname, BigDecimal balance) {
this.userId = userId;
this.nickname = nickname;
this.balance = balance;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getNickname() {
return nickname;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
public BigDecimal getBalance() {
return balance;
}
public void setBalance(BigDecimal balance) {
this.balance = balance;
}
}
With the YAML from the previous section, the toolkit finds this class and registers it as a reflective Compact class:
compactSerializationConfig.addClass(UserProfile.class);
You do not write that line in the application. The annotation keeps the serialization intent next to the type, and the toolkit applies it to the Hazelcast client configuration.
This mode is a good fit for straightforward cache values:
- DTO-like objects;
- map values with stable fields;
- internal service-to-service data where reflection-based schema inference is acceptable;
- early iterations where the model is still evolving.
Mode 2: explicit serializer registration
Sometimes reflective registration is not enough. You may want to control the Compact type name, field names, enum representation, backwards compatibility behavior, or cross-language contract.
For that, keep the same annotation and point it at a serializer:
package com.example.orders.model;
import io.github.javaquasar.hazelcast.toolkit.annotation.HzCompact;
import java.math.BigDecimal;
import java.time.Instant;
@HzCompact(serializer = OrderEntryCompactSerializer.class)
public class OrderEntry {
private String orderId;
private BigDecimal amount;
private Instant createdAt;
public OrderEntry() {
}
public OrderEntry(String orderId, BigDecimal amount, Instant createdAt) {
this.orderId = orderId;
this.amount = amount;
this.createdAt = createdAt;
}
public String getOrderId() {
return orderId;
}
public BigDecimal getAmount() {
return amount;
}
public Instant getCreatedAt() {
return createdAt;
}
}
The serializer is a normal Hazelcast CompactSerializer:
package com.example.orders.model;
import com.hazelcast.nio.serialization.compact.CompactReader;
import com.hazelcast.nio.serialization.compact.CompactSerializer;
import com.hazelcast.nio.serialization.compact.CompactWriter;
import java.math.BigDecimal;
import java.time.Instant;
public class OrderEntryCompactSerializer implements CompactSerializer<OrderEntry> {
public OrderEntryCompactSerializer() {
}
@Override
public OrderEntry read(CompactReader reader) {
String orderId = reader.readString("orderId");
BigDecimal amount = new BigDecimal(reader.readString("amount"));
Instant createdAt = Instant.ofEpochMilli(reader.readInt64("createdAtEpochMs"));
return new OrderEntry(orderId, amount, createdAt);
}
@Override
public void write(CompactWriter writer, OrderEntry order) {
writer.writeString("orderId", order.getOrderId());
writer.writeString("amount", order.getAmount().toPlainString());
writer.writeInt64("createdAtEpochMs", order.getCreatedAt().toEpochMilli());
}
@Override
public String getTypeName() {
return "order-entry";
}
@Override
public Class<OrderEntry> getCompactClass() {
return OrderEntry.class;
}
}
At startup, Hazelcast Toolkit:
- reads the serializer attribute from ***@HzCompact***;
- creates the serializer using its no-args constructor;
- validates that serializer.getCompactClass() matches the annotated class;
- registers the serializer before reflective classes.
That validation catches a surprisingly common copy-paste mistake:
@HzCompact(serializer = OrderEntryCompactSerializer.class)
public class InvoiceEntry {
}
If OrderEntryCompactSerializer#getCompactClass() returns OrderEntry.class, but the annotation is placed on InvoiceEntry, the application fails during startup instead of silently registering the wrong schema.
What happens internally
The runtime logic is deliberately small.
Conceptually, the toolkit does this:
public void registerCompactTypes(SerializationConfig serializationConfig, String basePackage) {
if (basePackage == null || basePackage.isBlank()) {
return;
}
CompactSerializationConfig compact =
serializationConfig.getCompactSerializationConfig();
Set<Class<?>> reflectiveClasses = new HashSet<>();
Set<CompactSerializer<?>> serializers = new HashSet<>();
for (Class<?> compactClass : classScanner.findAnnotated(basePackage, HzCompact.class)) {
HzCompact annotation = compactClass.getAnnotation(HzCompact.class);
Class<? extends CompactSerializer<?>> serializerClass = annotation.serializer();
if (HzCompact.NoopCompactSerializer.class.equals(serializerClass)) {
reflectiveClasses.add(compactClass);
continue;
}
CompactSerializer<?> serializer = instantiate(serializerClass);
validateSerializer(serializerClass, serializer, compactClass);
serializers.add(serializer);
}
serializers.forEach(compact::addSerializer);
reflectiveClasses.forEach(compact::addClass);
}
The real implementation also logs how many types were registered:
Registered 6 @HzCompact types from basePackage=com.example.app.model (serializers=2, reflectiveClasses=4)
This gives you a useful startup check. If the count is zero, the most likely cause is an incorrect base-package.
Why serializers are registered first
Hazelcast supports both reflective Compact classes and explicit Compact serializers. When both styles exist in the same application, explicit serializers should be applied first.
Hazelcast Toolkit keeps that order:
serializers.forEach(compact::addSerializer);
compactClasses.forEach(compact::addClass);
That lets the application mix both styles:
@HzCompact
public class UserProfile {
}
@HzCompact(serializer = OrderEntryCompactSerializer.class)
public class OrderEntry {
}
The simple classes stay simple. The classes that need a precise schema get one.
Using it with IMap
Once the Compact types are registered, you can use them with a regular Hazelcast IMap:
package com.example.profile;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.map.IMap;
import com.example.profile.model.UserProfile;
import org.springframework.stereotype.Service;
@Service
public class UserProfileCache {
private final IMap<String, UserProfile> users;
public UserProfileCache(HazelcastInstance hazelcastInstance) {
this.users = hazelcastInstance.getMap("users");
}
public void put(UserProfile profile) {
users.put(profile.getUserId(), profile);
}
public UserProfile get(String userId) {
return users.get(userId);
}
}
The service does not know how Compact registration happened. That is the point. Business code should use the cache, not maintain the serialization registry.
Common mistakes
Wrong base package
If your class is here:
com.example.profile.cache.UserProfile
But your YAML says:
hazelcast:
toolkit:
compact:
base-package: com.example.profile.model
The scanner will not find it.
Prefer a package that covers your cache models:
hazelcast:
toolkit:
compact:
base-package: com.example.profile
Missing no-args constructor on explicit serializer
Serializer classes are instantiated by the toolkit, so they need a no-args constructor:
public class UserProfileCompactSerializer implements CompactSerializer<UserProfile> {
public UserProfileCompactSerializer() {
}
// read/write/getTypeName/getCompactClass
}
If construction fails, startup fails with an error that points to the serializer class.
Serializer returns the wrong class
This is invalid:
@Override
public Class<OrderEntry> getCompactClass() {
return OrderEntry.class;
}
If the serializer is declared on InvoiceEntry, the toolkit rejects it. The annotation and getCompactClass() must describe the same type.
Treating scanning as magic
Package scanning is still a contract. Keep Compact cache models in predictable packages, and avoid scattering them across unrelated modules unless those modules are intentionally part of the scanned area.
When to use reflective vs explicit registration
Use reflective ***@HzCompact*** when:
- the class is a simple value object;
- field names and types are straightforward;
- the class is internal to one Java/Spring ecosystem;
- you want minimal code.
Use ***@HzCompact(serializer = …) ***when:
- you need stable cross-language schema control;
- you want custom field names or transformations;
- you need explicit enum/string/numeric encoding;
- you are evolving data across versions;
- you want serialization behavior to be reviewed as code.
The practical rule is: start reflective, move to explicit when the binary contract becomes part of your public or long-lived data model.
Why this belongs in a Spring Boot starter
Compact serialization is infrastructure. Most teams do not want every service to rediscover the same registration pattern.
A Spring Boot-friendly integration should:
- bind configuration from application.yml;
- scan application packages;
- register Compact classes before the client starts;
- validate explicit serializers early;
- let business services use HazelcastInstance normally.
That is what Hazelcast Toolkit provides. It keeps the low-level Hazelcast API available, but moves the repetitive registration code into the starter.
You still have control when you need it. You just do not have to pay for that control with manual config in every service.
Complete minimal example
Configuration:
spring:
application:
name: demo-service
hazelcast:
client:
cluster-name: dev
network:
cluster-members:
- 127.0.0.1:5701
toolkit:
compact:
base-package: com.example.demo.cache
Model:
package com.example.demo.cache;
import io.github.javaquasar.hazelcast.toolkit.annotation.HzCompact;
@HzCompact
public class AccountSnapshot {
private String accountId;
private long version;
public AccountSnapshot() {
}
public AccountSnapshot(String accountId, long version) {
this.accountId = accountId;
this.version = version;
}
public String getAccountId() {
return accountId;
}
public void setAccountId(String accountId) {
this.accountId = accountId;
}
public long getVersion() {
return version;
}
public void setVersion(long version) {
this.version = version;
}
}
Cache service:
package com.example.demo.cache;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.map.IMap;
import org.springframework.stereotype.Service;
@Service
public class AccountSnapshotCache {
private final IMap<String, AccountSnapshot> snapshots;
public AccountSnapshotCache(HazelcastInstance hazelcastInstance) {
this.snapshots = hazelcastInstance.getMap("account-snapshots");
}
public void save(AccountSnapshot snapshot) {
snapshots.put(snapshot.getAccountId(), snapshot);
}
public AccountSnapshot find(String accountId) {
return snapshots.get(accountId);
}
}
No custom ClientConfig bean. No central serializer registry. No Serializable marker on the domain class.
Just one package setting and one annotation.
Resources
GitHub repository: https://github.com/javaquasar/hazelcast-spring-toolkit
Maven Central: https://central.sonatype.com/artifact/io.github.javaquasar/hazelcast-toolkit-spring-boot3/0.1.1
Runnable Spring Boot 3 example: example-spring-boot3
Gradle:
implementation 'io.github.javaquasar:hazelcast-toolkit-spring-boot3:0.1.1'
Maven:
<dependency>
<groupId>io.github.javaquasar</groupId>
<artifactId>hazelcast-toolkit-spring-boot3</artifactId>
<version>0.1.1</version>
</dependency>
Closing thought
The best infrastructure code is often the code you do not have to keep copying.
Hazelcast Compact serialization is a strong fit for Spring Boot services, but manual registration makes it feel heavier than it should. Annotation-driven registration gives you a better shape: the model declares its serialization intent, the starter applies it to the Hazelcast client, and the application stays focused on its cache behavior.
That is the small idea behind ***@HzCompact.***
Make the common path boring. Keep the advanced path explicit. Fail fast when the wiring is wrong.
Previous: Part 1 — Zero-Boilerplate Hazelcast Client for Spring Boot Next: Part 3 — IMap listeners as Spring beans
메타데이터
- post_id
- 2c54f3902ed7
- slug
- auto-registering-hazelcast-compact-serialization-in-spring-boo-2c54f3902ed7
- url
- https://medium.com/@artur.buzov/auto-registering-hazelcast-compact-serialization-in-spring-boo-2c54f3902ed7
- canonical_url
- https://medium.com/@artur.buzov/auto-registering-hazelcast-compact-serialization-in-spring-boo-2c54f3902ed7
- author_url
- https://medium.com/@artur.buzov
- status
- ok
- fetched_at
- 2026-07-24 15:22:40