spring-boot-admin-docs/src/site/docs/02-server/40-instance-registry.md
The Instance Registry is the core component responsible for managing registered applications in Spring Boot Admin. It
uses an event-sourced architecture to track application state through the InstanceRepository interface.
The InstanceRepository is the primary interface for storing and retrieving application instances. It provides reactive
methods for managing instance lifecycle:
public interface InstanceRepository {
Mono<Instance> save(Instance app);
Flux<Instance> findAll();
Mono<Instance> find(InstanceId id);
Flux<Instance> findByName(String name);
Mono<Instance> compute(InstanceId id,
BiFunction<InstanceId, Instance, Mono<Instance>> remappingFunction);
Mono<Instance> computeIfPresent(InstanceId id,
BiFunction<InstanceId, Instance, Mono<Instance>> remappingFunction);
}
Spring Boot Admin uses EventsourcingInstanceRepository, which rebuilds instance state from events stored in the
InstanceEventStore.
Instead of directly storing instance state, the repository stores events that represent state changes:
InstanceRegisteredEvent is createdpublic class EventsourcingInstanceRepository implements InstanceRepository {
private final InstanceEventStore eventStore;
@Override
public Mono<Instance> save(Instance instance) {
return eventStore.append(instance.getUnsavedEvents())
.then(Mono.just(instance.clearUnsavedEvents()));
}
@Override
public Mono<Instance> find(InstanceId id) {
return eventStore.find(id)
.collectList()
.filter(e -> !e.isEmpty())
.map(events -> Instance.create(id).apply(events));
}
@Override
public Flux<Instance> findAll() {
return eventStore.findAll()
.groupBy(InstanceEvent::getInstance)
.flatMap(f -> f.reduce(Instance.create(f.key()),
Instance::apply));
}
}
When an application registers, a new instance is created:
InstanceId id = idGenerator.generateId(registration);
Instance newInstance = Instance.create(id).register(registration);
repository.save(newInstance);
This generates an InstanceRegisteredEvent.
After registration, the server detects available actuator endpoints:
instance = instance.withEndpoints(detectedEndpoints);
repository.save(instance);
This generates an InstanceEndpointsDetectedEvent.
The server periodically polls health endpoints:
instance = instance.withStatusInfo(statusInfo);
repository.save(instance);
This generates an InstanceStatusChangedEvent when status changes.
Application info is periodically refreshed:
instance = instance.withInfo(info);
repository.save(instance);
This generates an InstanceInfoChangedEvent when info changes.
When an application shuts down or is removed:
instance = instance.deregister();
repository.save(instance);
This generates an InstanceDeregisteredEvent.
The repository uses optimistic locking to handle concurrent updates:
private final Retry retryOptimisticLockException = Retry.max(10)
.doBeforeRetry(s -> log.debug("Retrying after OptimisticLockingException",
s.failure()))
.filter(OptimisticLockingException.class::isInstance);
@Override
public Mono<Instance> compute(InstanceId id,
BiFunction<InstanceId, Instance, Mono<Instance>> remappingFunction) {
return find(id)
.flatMap(app -> remappingFunction.apply(id, app))
.switchIfEmpty(Mono.defer(() -> remappingFunction.apply(id, null)))
.flatMap(this::save)
.retryWhen(retryOptimisticLockException);
}
If two updates conflict (based on event version numbers), the operation is automatically retried up to 10 times.
Flux<Instance> instances = repository.findAll();
instances.subscribe(instance -> {
System.out.println("Instance: " + instance.getRegistration().getName());
});
Mono<Instance> instance = repository.find(instanceId);
instance.subscribe(inst -> {
System.out.println("Found: " + inst.getRegistration().getName());
});
Flux<Instance> instances = repository.findByName("my-application");
instances.subscribe(instance -> {
System.out.println("Instance ID: " + instance.getId());
});
The compute methods provide atomic read-modify-write operations:
Updates an instance or creates it if it doesn't exist:
repository.compute(instanceId, (id, instance) -> {
if (instance == null) {
// Create new instance
return Mono.just(Instance.create(id).register(registration));
} else {
// Update existing instance
return Mono.just(instance.withStatusInfo(newStatus));
}
}).subscribe();
Updates only if the instance exists:
repository.computeIfPresent(instanceId, (id, instance) -> {
return Mono.just(instance.withInfo(updatedInfo));
}).subscribe();
An Instance object contains:
public class Instance {
private final InstanceId id;
private final long version;
private final Registration registration;
private final boolean registered;
private final StatusInfo statusInfo;
private final Info info;
private final Endpoints endpoints;
private final BuildVersion buildVersion;
private final Tags tags;
private final List<InstanceEvent> unsavedEvents;
}
id: Unique identifier for the instanceversion: Event version for optimistic lockingregistration: Registration details (name, URL, metadata)registered: Whether the instance is currently registeredstatusInfo: Current health statusinfo: Application info from /actuator/infoendpoints: Discovered actuator endpointsbuildVersion: Application version from build-infotags: Custom tags for classificationunsavedEvents: Events pending persistenceInstance IDs are generated by InstanceIdGenerator implementations:
Generates stable IDs based on the service URL:
public class HashingInstanceUrlIdGenerator implements InstanceIdGenerator {
@Override
public InstanceId generateId(Registration registration) {
String serviceUrl = registration.getServiceUrl();
// Generate hash-based ID from URL
return InstanceId.of(hash(serviceUrl));
}
}
Uses Cloud Foundry's application instance ID:
public class CloudFoundryInstanceIdGenerator implements InstanceIdGenerator {
@Override
public InstanceId generateId(Registration registration) {
String cfInstanceId = registration.getMetadata()
.get("applicationId")
+ ":" + registration.getMetadata().get("instanceId");
return InstanceId.of(cfInstanceId);
}
}
Implement your own ID generation strategy:
@Component
public class CustomInstanceIdGenerator implements InstanceIdGenerator {
@Override
public InstanceId generateId(Registration registration) {
// Custom logic to generate instance ID
String customId = registration.getName()
+ "-" + UUID.randomUUID().toString();
return InstanceId.of(customId);
}
}
@Component
public class InstanceManager {
private final InstanceRepository repository;
public InstanceManager(InstanceRepository repository) {
this.repository = repository;
}
public Flux<String> getApplicationNames() {
return repository.findAll()
.filter(Instance::isRegistered)
.map(i -> i.getRegistration().getName())
.distinct();
}
}
Subscribe to the event store to react to instance changes:
@Component
public class InstanceChangeListener {
public InstanceChangeListener(InstanceEventStore eventStore,
InstanceRepository repository) {
eventStore.subscribe(event -> {
if (event instanceof InstanceStatusChangedEvent statusEvent) {
repository.find(event.getInstance())
.subscribe(instance -> {
log.info("Instance {} status: {}",
instance.getRegistration().getName(),
instance.getStatusInfo().getStatus());
});
}
});
}
}