AppInitializationStateService.java
package com.taxonomy.shared.service;
import org.springframework.stereotype.Service;
import java.time.Instant;
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 {
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) {}
private final AtomicReference<StateSnapshot> snapshot =
new AtomicReference<>(new StateSnapshot(State.STARTING, "Starting application", Instant.now(), null));
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) {
snapshot.set(new StateSnapshot(newState, newMessage, Instant.now(), null));
}
public void fail(String newMessage, Throwable t) {
snapshot.set(new StateSnapshot(State.FAILED, newMessage, Instant.now(),
t == null ? null : t.getMessage()));
}
}