Skip to main content

Instance Registry

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.

InstanceRepository​

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);
}

Event-Sourced Implementation​

Spring Boot Admin uses EventsourcingInstanceRepository, which rebuilds instance state from events stored in the InstanceEventStore.

How It Works​

Instead of directly storing instance state, the repository stores events that represent state changes:

  1. Registration: When an application registers, an InstanceRegisteredEvent is created
  2. State Changes: Each state change (health, info, endpoints) generates a new event
  3. 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));
}
}

Benefits of Event Sourcing​

  • 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

Instance Lifecycle​

1. Registration​

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.

2. Endpoint Detection​

After registration, the server detects available actuator endpoints:

instance = instance.withEndpoints(detectedEndpoints);
repository.save(instance);

This generates an InstanceEndpointsDetectedEvent.

3. Status Updates​

The server periodically polls health endpoints:

instance = instance.withStatusInfo(statusInfo);
repository.save(instance);

This generates an InstanceStatusChangedEvent when status changes.

4. Info Updates​

Application info is periodically refreshed:

instance = instance.withInfo(info);
repository.save(instance);

This generates an InstanceInfoChangedEvent when info changes.

5. Deregistration​

When an application shuts down or is removed:

instance = instance.deregister();
repository.save(instance);

This generates an InstanceDeregisteredEvent.

Optimistic Locking​

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.

Querying Instances​

Find All Instances​

Flux<Instance> instances = repository.findAll();
instances.subscribe(instance -> {
System.out.println("Instance: " + instance.getRegistration().getName());
});

Find by Instance ID​

Mono<Instance> instance = repository.find(instanceId);
instance.subscribe(inst -> {
System.out.println("Found: " + inst.getRegistration().getName());
});

Find by Application Name​

Flux<Instance> instances = repository.findByName("my-application");
instances.subscribe(instance -> {
System.out.println("Instance ID: " + instance.getId());
});

Compute Operations​

The compute methods provide atomic read-modify-write operations:

compute()​

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();

computeIfPresent()​

Updates only if the instance exists:

repository.computeIfPresent(instanceId, (id, instance) -> {
return Mono.just(instance.withInfo(updatedInfo));
}).subscribe();

Instance State​

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;
}

Key Properties​

  • id: Unique identifier for the instance
  • version: Event version for optimistic locking
  • registration: Registration details (name, URL, metadata)
  • registered: Whether the instance is currently registered
  • statusInfo: Current health status
  • info: Application info from /actuator/info
  • endpoints: Discovered actuator endpoints
  • buildVersion: Application version from build-info
  • tags: Custom tags for classification
  • unsavedEvents: Events pending persistence

Instance ID Generation​

Instance IDs are generated by InstanceIdGenerator implementations:

Default: HashingInstanceUrlIdGenerator​

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));
}
}

Cloud Foundry: CloudFoundryInstanceIdGenerator​

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);
}
}

Custom ID Generator​

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);
}
}

Working with the Repository​

Injecting the Repository​

@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();
}
}

Reacting to Changes​

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());
});
}
});
}
}

Best Practices​

  1. Use compute methods for atomic updates to avoid race conditions
  2. Don't modify Instance objects directly - use the builder-style methods (withXxx)
  3. Let the system retry optimistic locking failures automatically
  4. Subscribe to events for reactive processing instead of polling
  5. Use findByName for multi-instance applications to find all instances of a service

See Also​