| sidebar_position | 40 | ||
|---|---|---|---|
| sidebar_custom_props |
|
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:
- Registration: When an application registers, an
InstanceRegisteredEventis created - State Changes: Each state change (health, info, endpoints) generates a new event
- Reconstruction: The current instance state is rebuilt by replaying all events
public 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));
}
}- Complete Audit Trail: Every change is recorded as an event
- Temporal Queries: Can reconstruct state at any point in time
- Event Replay: Can rebuild state from events after crashes
- Debugging: Full history of state changes for troubleshooting
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 persistence
Instance 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());
});
}
});
}
}- Use compute methods for atomic updates to avoid race conditions
- Don't modify Instance objects directly - use the builder-style methods (withXxx)
- Let the system retry optimistic locking failures automatically
- Subscribe to events for reactive processing instead of polling
- Use findByName for multi-instance applications to find all instances of a service
- Persistence - Learn about event storage
- Events - Understand the event system
- Clustering - Distributed instance management