Explain hierarchical (nested) states and orthogonal regions in Spring Statemachine. When would you use each?
answer
- Hierarchy: substate implies parent; parent transitions shared
- withStates().parent(P).initial(child)
- Regions = multiple withStates() same parent = parallel
- Fork splits into regions, join synchronises
- getStates() (plural) for parallel machines
basics
~20 sHierarchical states nest substates inside a parent (superstate), so shared behaviour and transitions live on the parent and substates specialise it. Regions are orthogonal (parallel) sub-machines inside one state that are active simultaneously — the machine is in several states at once.
solid answer
~50 sBoth come from UML statecharts and tame state explosion. Hierarchical (nested/composite) states put substates inside a superstate: you configure a child withStates().parent(PARENT).initial(...). Being in a substate means also being in its parent, so a transition defined on the parent applies to all substates (shared/default behaviour), and entering the parent drops into the child's initial state. Local transitions (withLocal) move within the hierarchy without exiting/re-entering the parent. Orthogonal regions model concurrency: a single composite state contains two or more independent regions, each its own little machine with its own current substate, all active at once — so getState()/getStates() can report multiple states. You enter/leave regions together, often via fork and join pseudostates. Use hierarchy for shared behaviour and specialisation (e.g. a CONNECTED superstate with IDLE/BUSY substates); use regions when genuinely independent concerns run in parallel (e.g. a media player tracking playback and network status simultaneously).
code
java · 21 lines// Hierarchy: CONNECTED superstate with IDLE/BUSY substates,
// plus orthogonal region for a parallel concern.
@Override
public void configure(StateMachineStateConfigurer<S, E> states) throws Exception {
states
.withStates()
.initial(S.OFFLINE)
.state(S.CONNECTED)
.and()
.withStates() // region 1: session substates of CONNECTED
.parent(S.CONNECTED)
.initial(S.IDLE)
.state(S.BUSY)
.and()
.withStates() // region 2: signal substates of CONNECTED (parallel)
.parent(S.CONNECTED)
.initial(S.WEAK)
.state(S.STRONG);
// While CONNECTED, the machine is simultaneously in one of {IDLE,BUSY}
// AND one of {WEAK,STRONG}; getStates() reports all active states.
}go deeper
Aware that states can be nested and that some machines can be in more than one state.
Can configure a parent/child hierarchy and knows substate implies parent and inherits its transitions.
Distinguishes hierarchy (share/specialise) from regions (parallelism), uses fork/join and local transitions, and reads getStates() for parallel machines.
Chooses hierarchy/regions to prevent state explosion, designs fork/join synchronisation carefully, and ensures persistence captures all region states.
## Why these exist A flat FSM suffers **state explosion**: if two independent concerns each have N states, modelling them together needs N×N combined states, and shared behaviour gets copy-pasted across many transitions. UML statecharts solve this with **hierarchy** (to share/specialise) and **orthogonal regions** (to run concerns in parallel). Spring Statemachine implements both. ## Hierarchical (nested / composite) states A **composite (super)state** contains **substates**. You configure the nesting by declaring a second `withStates()` block that names a **parent**: ```java states .withStates() .initial(States.CONNECTED) .state(States.CONNECTED) .and() .withStates() .parent(States.CONNECTED) // <-- nests these inside CONNECTED .initial(SubStates.IDLE) .state(SubStates.BUSY); ``` Key semantics: - **Being in a substate implies being in the parent.** So `getStates()` reports both, e.g. `{CONNECTED, IDLE}`. - **Entering the composite state** drops into that region's **initial substate** automatically. - **Transitions on the parent are inherited/shared:** a transition `CONNECTED --DISCONNECT--> DISCONNECTED` fires from *any* substate — you write the common behaviour once. Substates can override with more specific transitions. - This gives **specialisation**: the parent holds default/common behaviour; children add or override specifics. ### External vs local vs internal in a hierarchy - **External** transition between parent and child **exits and re-enters** the parent (running its exit then entry actions). - **Local** (`withLocal()`) transition moves between a superstate and a substate **without** exiting/re-entering the shared superstate — so the parent's entry/exit actions don't re-run. Use it when you want to change substate without disturbing the parent. - **Internal** runs an action without leaving the current state at all. ## Orthogonal regions (parallelism) A state can contain **multiple regions** that are **all active simultaneously** — the machine is in *several* states at once. You create regions by declaring **multiple `withStates()` blocks that share the same parent** (each block with its own `.initial(...)`). Each region is effectively an independent sub-machine with its own current substate. Consequences: - `getStates()` returns a **set** of states (one per active region), and `getState()` is only meaningfully single for flat machines. - An event is offered to **all** regions; each region independently decides whether it has a matching transition. - Regions are typically entered and exited together via **fork** and **join** pseudostates: a **fork** splits one incoming transition into the initial states of several regions; a **join** waits for all regions to reach designated states before proceeding. ### Pseudostates that show up here - **Fork** — enters multiple regions at once. - **Join** — synchronises: fires only when every joined region has reached its target state. - (Also **choice/junction** for guard-based branching, **history** to re-enter a composite state at its last active substate — related statechart features.) ## When to use which - **Hierarchy**: when several states share behaviour or a common transition (a cancel/disconnect/error that applies everywhere within a superstate), or when you want a general mode refined by sub-modes. Reduces duplicated transitions. - **Regions**: when the system tracks **genuinely independent concerns concurrently** — e.g. a media player where *playback* (playing/paused/stopped) and *connection* (online/offline) evolve independently, or a UI form where several sections validate in parallel. Avoids the N×M combinatorial state blow-up. ## Gotchas - **Regions mean multiple current states** — code that assumes a single `getState()` breaks; use `getStates()`. - **Forgetting an initial substate** in a composite/region — configuration/entry fails; every region needs one. - **Join synchronisation** — a join won't fire until *all* its regions reach the required states; a stuck region blocks it. - **Local vs external confusion** — using external where you wanted local causes the parent's entry/exit actions to re-run unexpectedly. - **Persisting a parallel machine** — you must capture *all* region states in the `StateMachineContext`, not one.
- How is a transition defined on a superstate related to its substates?It is shared: because being in a substate means also being in the parent, a transition declared on the parent fires from any substate. This lets you write common behaviour (e.g. a global DISCONNECT) once instead of on every child.
- How do you enter and synchronise multiple orthogonal regions?Use a fork pseudostate to enter several regions simultaneously (splitting into each region's target substate), and a join pseudostate to synchronise — the join fires only once every joined region has reached its designated state.
- What's the difference between a local and an external transition in a hierarchy?An external transition between parent and child exits and re-enters the parent, re-running its exit/entry actions; a local transition changes substate without exiting/re-entering the shared parent, so those actions don't re-run.
saying these in an interview costs you the question
- Assuming a machine is always in exactly one state, even with orthogonal regions
- Confusing hierarchy (sharing/specialisation) with regions (parallelism)
- Using external transitions in a hierarchy and being surprised the parent's entry/exit re-run
- Forgetting to give each region/composite state an initial substate