ADR-0014: Generic State Machine in Shared Kernel

Status

Accepted

Tags

fieldforce, state-machine, shared-kernel, ddd, reuse

Decision

The generic StateMachine[TState, TEvent comparable] type lives at internal/shared/domain/statemachine/, not inside the fieldforce module. It uses a pure-function style: Trigger(ctx, currentState, event, transitionContext) → (newState, error). The fieldforce task machine registers its 9 transitions using this shared type at internal/modules/fieldforce/domain/statemachine/task_machine.go.

Why

Fieldforce was the first module to need a state machine. Placing it inside the fieldforce module has zero cost today, but the moment a second module (leave approval, order lifecycle) needs the same primitive, every import path referencing the fieldforce-local type must change. In a monorepo with a single Go module, placing it in the shared kernel costs the same now and avoids migration later. Rejected alternatives:
  • Fieldforce-local state machine: Zero cost upfront. Migration to shared requires touching every module that adopted the fieldforce-local path. Rejected.
  • Interface-only shared kernel, implementations per module: The value is the Trigger implementation (guard execution, error handling, transition lookup), not just the interface. Duplicating it per module defeats the purpose. Rejected.

How it works

Trigger flow:
TransitionContext is map[string]any carrying role, review_note, user IDs. The machine never persists anything — it is a pure computation.

Rules for agents

  • Any module that models lifecycle state transitions MUST use internal/shared/domain/statemachine/StateMachine — do not create module-local machine types
  • Trigger is a pure function — never call repo.Update or any I/O inside a Guard or Action; return the new state and let the service layer persist
  • ErrInvalidTransition is the sentinel for all guard failures — callers type-assert to extract From and Event for error messages
  • Do not add role checks in the HTTP handler that duplicate guard logic in the task machine’s transitions

Bad pattern (do not generate)

Good pattern