AppInitializationStateService.java
package com.taxonomy.shared.service;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.stereotype.Service;
import java.time.Instant;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
/**
* Centralises application initialisation state tracking.
*
* <p>The state machine follows this happy path:
* {@code STARTING} → {@code LOADING_TAXONOMY} → {@code BUILDING_INDEX} → {@code READY}
*
* <p>Any phase can transition to {@code FAILED} via {@link #fail(String, Throwable)}.
*
* <p>All mutable fields are wrapped in an immutable {@link StateSnapshot} and stored in a
* single {@link AtomicReference} to guarantee a consistent view across threads.
*/
@Service
public class AppInitializationStateService
implements ApplicationEventPublisherAware {
public enum State {
STARTING,
LOADING_TAXONOMY,
BUILDING_INDEX,
READY,
FAILED
}
/** Immutable snapshot of the current initialisation state. */
public record StateSnapshot(
State state,
String message,
Instant updatedAt,
String error) {
}
/** Emitted synchronously after the application enters the READY state. */
public record InitializationReadyEvent(StateSnapshot snapshot) {
public InitializationReadyEvent {
snapshot = Objects.requireNonNull(snapshot, "snapshot");
}
}
private final AtomicReference<StateSnapshot> snapshot =
new AtomicReference<>(new StateSnapshot(
State.STARTING,
"Starting application",
Instant.now(),
null));
private volatile ApplicationEventPublisher eventPublisher;
@Override
public void setApplicationEventPublisher(
ApplicationEventPublisher applicationEventPublisher) {
this.eventPublisher = Objects.requireNonNull(
applicationEventPublisher,
"applicationEventPublisher");
}
public State getState() {
return snapshot.get().state();
}
public String getMessage() {
return snapshot.get().message();
}
public Instant getUpdatedAt() {
return snapshot.get().updatedAt();
}
public String getError() {
return snapshot.get().error();
}
public boolean isReady() {
return snapshot.get().state() == State.READY;
}
public void update(State newState, String newMessage) {
StateSnapshot updated = new StateSnapshot(
newState,
newMessage,
Instant.now(),
null);
snapshot.set(updated);
ApplicationEventPublisher publisher = eventPublisher;
if (newState == State.READY && publisher != null) {
publisher.publishEvent(new InitializationReadyEvent(updated));
}
}
public void fail(String newMessage, Throwable t) {
snapshot.set(new StateSnapshot(
State.FAILED,
newMessage,
Instant.now(),
t == null ? null : t.getMessage()));
}
}