Skip to main content

Persistence and Event Store

Spring Boot Admin uses an event-sourced architecture to track the state of registered applications. All changes to application instances are stored as events in an InstanceEventStore, allowing the server to rebuild application state and maintain a complete audit trail.

Event Store Architecture​

The InstanceEventStore is responsible for storing all instance-related events. Spring Boot Admin provides two built-in implementations:

InMemoryEventStore​

The default implementation stores events in memory using a ConcurrentHashMap. This is suitable for single-instance deployments and development environments.

Characteristics:

  • Fast and lightweight
  • Non-persistent (data lost on restart)
  • Limited by available memory
  • Configurable maximum log size per instance

Configuration:

@Bean
public InstanceEventStore eventStore() {
return new InMemoryEventStore(100); // Max 100 events per instance
}

The default configuration creates an InMemoryEventStore with a maximum of 100 events per instance aggregate. Older events are automatically removed when the limit is reached.

HazelcastEventStore​

For clustered deployments, the HazelcastEventStore provides distributed persistence using Hazelcast's IMap.

Characteristics:

  • Distributed across cluster nodes
  • Survives single-node failures
  • Automatic synchronization between nodes
  • Real-time event publishing across the cluster

Configuration:

First, add the Hazelcast dependency:

pom.xml
<dependency>
<groupId>com.hazelcast</groupId>
<artifactId>hazelcast</artifactId>
</dependency>

Then configure the Hazelcast-backed event store:

import com.hazelcast.config.Config;
import com.hazelcast.config.MapConfig;
import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.map.IMap;
import de.codecentric.boot.admin.server.eventstore.HazelcastEventStore;

@Configuration
public class HazelcastConfig {

@Bean
public Config hazelcastConfig() {
MapConfig mapConfig = new MapConfig("spring-boot-admin-event-store")
.setBackupCount(1)
.setMergePolicyConfig(new MergePolicyConfig(
PutIfAbsentMergePolicy.class.getName(), 100));

Config config = new Config();
config.addMapConfig(mapConfig);
return config;
}

@Bean
public HazelcastInstance hazelcastInstance(Config hazelcastConfig) {
return Hazelcast.newHazelcastInstance(hazelcastConfig);
}

@Bean
public InstanceEventStore eventStore(HazelcastInstance hazelcastInstance) {
IMap<InstanceId, List<InstanceEvent>> map =
hazelcastInstance.getMap("spring-boot-admin-event-store");
return new HazelcastEventStore(100, map);
}
}

How it works:

The HazelcastEventStore listens to map entry updates and publishes new events to all cluster nodes:

eventLog.addEntryListener(new EntryAdapter<InstanceId, List<InstanceEvent>>() {
@Override
public void entryUpdated(EntryEvent<InstanceId, List<InstanceEvent>> event) {
long lastKnownVersion = getLastVersion(event.getOldValue());
List<InstanceEvent> newEvents = event.getValue()
.stream()
.filter((e) -> e.getVersion() > lastKnownVersion)
.toList();
publish(newEvents);
}
}, true);

Event Types​

The event store manages different types of instance events:

  • InstanceRegisteredEvent - Application registers with the server
  • InstanceDeregisteredEvent - Application unregisters or is removed
  • InstanceStatusChangedEvent - Health status changes
  • InstanceEndpointsDetectedEvent - Actuator endpoints discovered
  • InstanceInfoChangedEvent - Application info updated
  • InstanceRegistrationUpdatedEvent - Registration details changed

Each event contains:

  • Instance ID
  • Timestamp
  • Version (for optimistic locking)
  • Event-specific data

InstanceEventStore Interface​

public interface InstanceEventStore extends Publisher<InstanceEvent> {

Flux<InstanceEvent> findAll();

Flux<InstanceEvent> find(InstanceId id);

Mono<Void> append(List<InstanceEvent> events);
}

Methods​

  • findAll() - Returns all events for all instances
  • find(InstanceId id) - Returns events for a specific instance
  • append(List<InstanceEvent> events) - Appends new events to the store

The store also implements Publisher<InstanceEvent>, allowing components to subscribe to new events in real-time.

Event Versioning and Optimistic Locking​

Events are versioned to prevent concurrent modification issues. Each event includes a version number that increments with each change:

public abstract class InstanceEvent implements Serializable {
private final InstanceId instance;
private final long version;
private final long timestamp;

// ...
}

When appending events, the event store checks that the version matches the expected sequence, throwing an OptimisticLockingException if there's a conflict.

Event Publishing​

The event store publishes events to subscribers, enabling reactive processing:

eventStore.subscribe(event -> {
if (event instanceof InstanceStatusChangedEvent statusEvent) {
// React to status changes
System.out.println("Instance " + event.getInstance() +
" changed to " + statusEvent.getStatusInfo().getStatus());
}
});

Configuring Event Store Size​

Control the maximum number of events stored per instance:

@Bean
public InstanceEventStore eventStore() {
return new InMemoryEventStore(500); // Store up to 500 events per instance
}

When the limit is reached, the oldest events are removed. This prevents unbounded memory growth while maintaining recent history.

Custom Event Store Implementation​

You can implement your own event store for custom persistence requirements (e.g., database, external cache):

public class CustomEventStore implements InstanceEventStore {

@Override
public Flux<InstanceEvent> findAll() {
// Load all events from your storage
}

@Override
public Flux<InstanceEvent> find(InstanceId id) {
// Load events for specific instance
}

@Override
public Mono<Void> append(List<InstanceEvent> events) {
// Persist events and publish to subscribers
}

@Override
public void subscribe(Subscriber<? super InstanceEvent> subscriber) {
// Handle event subscriptions
}
}

Then register your custom implementation as a bean:

@Bean
public InstanceEventStore eventStore() {
return new CustomEventStore();
}

Best Practices​

  1. For Development: Use InMemoryEventStore for simplicity
  2. For Single Instance Deployments: Use InMemoryEventStore if restart data loss is acceptable
  3. For Clustered Deployments: Use HazelcastEventStore for high availability
  4. For Large Deployments: Tune the max log size to balance memory usage and history retention
  5. For Custom Requirements: Implement your own event store with database or distributed cache backing

Monitoring Event Store​

Monitor event store health through actuator endpoints or by subscribing to events:

@Component
public class EventStoreMonitor {

public EventStoreMonitor(InstanceEventStore eventStore) {
eventStore.subscribe(event -> {
// Log or metric collection
log.debug("Event: {} for instance {}",
event.getType(), event.getInstance());
});
}
}

See Also​