Sitemap
Manish Pandey

Manish Pandey

Software Engineer at Mastercard

Spring StateMachine: From Finite State Machines to Production-Ready Payment Flows

15 min readSep 22, 2026

--

A finite state machine (FSM) is a general computer-science model used to represent systems that move between a fixed set of states.

Spring Statemachine brings that idea into the Spring ecosystem and gives us a way to model those state transitions in Java applications.

A payment lifecycle is a good example.

A transaction might start like this:

CREATED
↓
PENDING
↓
AUTHORIZED
↓
COMPLETED

There are other possible paths too:

PENDING
↓
FAILED

or:

PENDING
↓
CANCELLED

The key idea is that a transaction cannot move from one state to another arbitrarily.

For example, allowing:

COMPLETED → PENDING

would probably violate the business rules of the payment system.

An FSM gives us a structured way to define which movements are allowed and which aren’t.

1. What is a Finite State Machine?

An FSM models a system using a finite number of states and predefined transitions between them.

A typical FSM has four core concepts:

  • State
  • Event
  • Transition
  • Action

Let’s see what each one means in a payment system.

State

A state describes where the transaction currently is in its lifecycle.

For example:

CREATED
PENDING
AUTHORIZED
COMPLETED
FAILED
CANCELLED

Event

An event represents something that happened or something that was requested.

Examples:

SUBMIT
AUTHORIZE
AUTH_SUCCESS
AUTH_FAILED
COMPLETE
CANCEL

Transition

A transition defines how an event moves an entity from one state to another.

For example:

PENDING + AUTH_SUCCESS
↓
AUTHORIZED

Action

An action is work that needs to happen when a transition occurs.

For example:

PENDING
|
| AUTH_SUCCESS
↓
AUTHORIZED
|
+--> update database
+--> publish event
+--> write audit record

2. The mental model

A simple way to think about an FSM is:

Current State
+
Event
↓
State Machine
↓
Next State
+
Action

For example:

PENDING + AUTH_SUCCESS
↓
AUTHORIZED

The state machine looks at the current state and the incoming event, checks whether that combination has a valid transition, and then moves the entity to the next state.

3. States and events are not the same thing

This is one of the first distinctions you should get comfortable with.

Consider:

AUTHORIZED

This describes the current condition of a payment, so it is a state.

Now consider:

AUTH_SUCCESS

This describes something that happened, so it is an event.

A useful way to remember it is:

State = Where am I?
Event = What just happened?

For example:

AUTHORIZING
+
AUTH_SUCCESS
↓
AUTHORIZED

4. Why not just use if/else?

For a small application, there is nothing inherently wrong with using conditions.

You could write:

if (status.equals("PENDING")) {
    if (event.equals("AUTH_SUCCESS")) {
status = "AUTHORIZED";
}
else if (event.equals("CANCEL")) {
status = "CANCELLED";
}
}
else if (status.equals("AUTHORIZED")) {
if (event.equals("COMPLETE")) {
status = "COMPLETED";
}
}

This works.

The problem starts when the lifecycle becomes more complicated.

Imagine a payment system with 20 states and dozens of possible events.

Eventually you can end up with deeply nested conditions spread across multiple services:

if
if
if
else if
if
else

At that point, answering a simple question such as:

“Can a payment in AUTHORIZING be cancelled?”

may require searching through several classes.

With an FSM, the business rules can be represented directly:

PENDING + AUTH_SUCCESS       → AUTHORIZED
PENDING + CANCEL → CANCELLED
AUTHORIZED + START_PROCESSING → PROCESSINGPROCESSING + PROCESS_SUCCESS  → COMPLETED
PROCESSING + PROCESS_FAILED → FAILED

The state model becomes much easier to inspect and test.

5. Designing a payment state machine

Let’s build a slightly more realistic payment lifecycle.

States

public enum PaymentState {
    CREATED,
PENDING,
AUTHORIZING,
AUTHORIZED,
PROCESSING,
COMPLETED,
FAILED,
CANCELLED
}

Events

public enum PaymentEvent {
    SUBMIT,
START_AUTHORIZATION,
AUTH_SUCCESS,
AUTH_FAILED,
START_PROCESSING,
PROCESS_SUCCESS,
PROCESS_FAILED,
CANCEL
}

Now we can define the lifecycle.

CREATED
|
| SUBMIT
↓
PENDING
|
| START_AUTHORIZATION
↓
AUTHORIZING
|
+---- AUTH_SUCCESS ----> AUTHORIZED
|
+---- AUTH_FAILED -----> FAILED
PENDING
|
| CANCEL
↓
CANCELLED
AUTHORIZED
|
| START_PROCESSING
↓
PROCESSING
|
+---- PROCESS_SUCCESS --> COMPLETED
|
+---- PROCESS_FAILED ---> FAILED

Even without writing Java code, this diagram already describes a large part of the payment lifecycle.

6. Bringing Spring StateMachine into the picture

Spring Statemachine provides infrastructure for modeling these concepts in a Spring application.

Depending on the version you’re using, the API and configuration style can differ, so it’s worth checking the documentation for the specific Spring Statemachine release in your project.

The concepts themselves remain the same:

States
Events
Transitions
Actions
Guards
Listeners
Persistence

A typical project structure might look like this:

src/main/java
|
+-- state
| |
| +-- PaymentState.java
| +-- PaymentEvent.java
| +-- PaymentStateConfig.java
| +-- PaymentActions.java
| +-- PaymentGuards.java
|
+-- service
| |
| +-- PaymentService.java
|
+-- controller
|
+-- PaymentController.java

7. Defining the states

Our state enum is straightforward:

public enum PaymentState {
    CREATED,
PENDING,
AUTHORIZING,
AUTHORIZED,
PROCESSING,
COMPLETED,
FAILED,
CANCELLED
}

These represent the finite states through which a payment can move.

8. Defining events

The events are separate:

public enum PaymentEvent {
    SUBMIT,
START_AUTHORIZATION,
AUTH_SUCCESS,
AUTH_FAILED,
START_PROCESSING,
PROCESS_SUCCESS,
PROCESS_FAILED,
CANCEL
}

For example:

CREATED + SUBMIT
↓
PENDING

The event is SUBMIT.

The resulting state is PENDING.

Keeping this distinction clear makes the rest of the design much easier.

9. Defining transitions

Our transition rules look like this:

CREATED --SUBMIT--> PENDING
PENDING --START_AUTHORIZATION--> AUTHORIZINGAUTHORIZING --AUTH_SUCCESS--> AUTHORIZEDAUTHORIZING --AUTH_FAILED--> FAILEDPENDING --CANCEL--> CANCELLEDAUTHORIZED --START_PROCESSING--> PROCESSINGPROCESSING --PROCESS_SUCCESS--> COMPLETEDPROCESSING --PROCESS_FAILED--> FAILED

In a Spring Statemachine configuration, the transitions are expressed using the framework’s configuration API.

Conceptually, a transition looks like:

.withExternal()
.source(PaymentState.CREATED)
.target(PaymentState.PENDING)
.event(PaymentEvent.SUBMIT);

Another transition would look like:

.withExternal()
.source(PaymentState.PENDING)
.target(PaymentState.AUTHORIZING)
.event(PaymentEvent.START_AUTHORIZATION);

The precise configuration APIs can vary between Spring Statemachine versions, so don’t copy configuration code from an old tutorial without checking the version used by your application.

10. External and internal transitions

You may come across the terms external transition and internal transition.

An external transition changes the current state.

For example:

PENDING
↓
AUTHORIZED

An internal transition handles an event without changing the state.

For example:

PROCESSING
|
| HEARTBEAT
↓
PROCESSING

The payment remains in PROCESSING, but the event could trigger an action such as recording a heartbeat or updating monitoring information.

11. Actions

An action is work that runs when a transition occurs.

Suppose this happens:

AUTHORIZING
|
| AUTH_SUCCESS
↓
AUTHORIZED

After authorization succeeds, the application might need to:

  • save authorization details
  • write an audit record
  • publish a Kafka event
  • update metrics

Conceptually, an action can access information associated with the state machine:

Action<PaymentState, PaymentEvent> authorizationSuccess =
context -> {
        Payment payment =
context.getExtendedState()
.get("payment", Payment.class);
System.out.println(
"Payment authorized: " + payment.getId()
);
};

The actual implementation should be designed carefully around your transaction boundaries and persistence strategy.

12. Keep business side effects separate

It is tempting to put everything inside a transition configuration.

For example:

Transition
|
+--> update DB
+--> call payment gateway
+--> publish Kafka event
+--> send email
+--> write audit

That quickly becomes difficult to maintain.

A cleaner structure is:

Transition
↓
Action
↓
Business Service
|
+--> persistence
+--> messaging
+--> audit

The state machine describes the lifecycle. Your application services handle the actual business operations.

13. Guards

Sometimes an event should only be accepted when a particular condition is satisfied.

Suppose a payment is pending and we receive an authorization request.

Authorization should only proceed when:

payment amount <= available limit

A guard can represent that condition.

Conceptually:

amount <= limit
|
YES
↓
PENDING ----------> AUTHORIZED

If the condition isn’t satisfied, the transition isn’t taken.

A guard therefore answers:

“Is this transition allowed right now?”

14. Guard example

Conceptually:

Guard<PaymentState, PaymentEvent> amountGuard =
context -> {
        Payment payment =
context.getExtendedState()
.get("payment", Payment.class);
return payment.getAmount()
.compareTo(payment.getAvailableLimit()) <= 0;
};

The flow becomes:

Event
↓
Guard
|
+---- false → transition not taken
|
+---- true → transition happens

15. Guard vs Action

This distinction is worth remembering.

Guard

Determines whether a transition is allowed.

Can this happen?

Action

Runs work associated with the transition.

What should happen when it does?

So the flow is:

Event
↓
Guard
↓
Allowed?
|
+---- NO
|
+---- YES
↓
Transition
↓
Next State
↓
Action

16. Extended State

The current state isn’t always enough to represent everything the application needs to know.

Suppose the payment is currently:

AUTHORIZING

You might also need:

paymentId = TX123
attemptCount = 2
riskScore = 40

These aren’t separate states. They’re contextual information.

Spring Statemachine provides an extended-state mechanism for storing additional variables associated with the state machine.

Conceptually:

State Machine
|
+-- Current State
| |
| +-- AUTHORIZING
|
+-- Extended State
|
+-- paymentId
+-- attemptCount
+-- riskScore

17. Don’t create states for every piece of data

This is a common modeling mistake.

You wouldn’t normally want:

AUTHORIZING_ATTEMPT_1
AUTHORIZING_ATTEMPT_2
AUTHORIZING_ATTEMPT_3

Instead:

AUTHORIZING

and:

attemptCount = 3

The state describes the business lifecycle.

The additional data describes the context around that lifecycle.

18. State machine lifecycle

A state machine also has its own lifecycle.

A simplified view is:

Created
↓
Started
↓
Receive Events
↓
Execute Transitions
↓
Stopped

Once started, an event can be sent to the machine.

For example:

stateMachine.start();

Then:

stateMachine.sendEvent(PaymentEvent.SUBMIT);

The machine evaluates the current state and the event and determines whether a valid transition exists.

19. Walking through a complete payment

Suppose the initial state is:

CREATED

We send:

SUBMIT

The machine finds:

CREATED + SUBMIT
↓
PENDING

Next:

START_AUTHORIZATION

Result:

PENDING + START_AUTHORIZATION
↓
AUTHORIZING

Then:

AUTH_SUCCESS

Result:

AUTHORIZING + AUTH_SUCCESS
↓
AUTHORIZED

Next:

START_PROCESSING

Result:

AUTHORIZED + START_PROCESSING
↓
PROCESSING

Finally:

PROCESS_SUCCESS

Result:

PROCESSING + PROCESS_SUCCESS
↓
COMPLETED

The entire lifecycle becomes:

CREATED
↓ SUBMIT
PENDING
↓ START_AUTHORIZATION
AUTHORIZING
↓ AUTH_SUCCESS
AUTHORIZED
↓ START_PROCESSING
PROCESSING
↓ PROCESS_SUCCESS
COMPLETED

20. What happens when an invalid event arrives?

Suppose the current state is:

CREATED

and somebody sends:

PROCESS_SUCCESS

There is no transition defined for:

CREATED + PROCESS_SUCCESS

The state machine therefore doesn’t simply decide to execute arbitrary business logic.

The event isn’t a valid transition from the current state.

That’s one of the major advantages of explicitly modeling the lifecycle.

21. Why this works well for payments

Payments naturally have a lifecycle with business constraints.

For example:

CREATED
↓
PENDING
↓
AUTHORIZED
↓
PROCESSING
↓
COMPLETED

But certain movements shouldn’t normally be possible:

COMPLETED → CREATED
COMPLETED → PENDING
CANCELLED → PROCESSING

The state machine gives you a place to define those rules instead of leaving them scattered throughout the application.

22. FSM and the database status field

You might be wondering:

“If I already have payment.status in PostgreSQL, why do I need a state machine?”

The answer is that these solve different problems.

Your database can store:

payment
--------------------------------
id
amount
status
created_at
updated_at

For example:

id       status
TX100 AUTHORIZED

That’s useful because the payment state needs to survive application restarts.

The state machine, meanwhile, defines what transitions are legal.

Without a centralized model, you might have code like this spread throughout the application:

if (status == PENDING) {
...
}
if (status == AUTHORIZED) {
...
}
if (status == FAILED) {
...
}

The database stores the state.

The state machine manages the lifecycle rules around that state.

23. Persistence becomes important in production

Consider this scenario:

PENDING
↓
AUTH_SUCCESS
↓
AUTHORIZED

The application then crashes.

When the service starts again, it needs to know that the payment was already authorized.

That’s why the business state should normally be persisted.

For example:

Database
|
| status = AUTHORIZED
↓
State Machine
|
↓
AUTHORIZED

For more complex state-machine data, Spring Statemachine also provides persistence-related mechanisms.

But there is an architectural distinction worth keeping in mind:

state-machine persistence is not automatically the same thing as your business database.

For a payment system, the payment database should remain an intentional part of the source-of-truth design.

24. State transitions and database transactions

Consider this transition:

PENDING
↓
AUTH_SUCCESS
↓
AUTHORIZED

Maybe the application needs to:

  1. update the payment
  2. save authorization details
  3. create an audit record
  4. publish a domain event

You need to decide where the transaction boundary belongs.

For example:

Database Transaction
|
+-- update payment
+-- save authorization
+-- write audit
|
COMMIT

Publishing to Kafka is a separate concern.

Get Manish Pandey’s stories in your inbox

Join Medium for free to get updates from this writer.

A robust architecture may use an outbox pattern so the database change and event publication can be coordinated reliably.

You should not assume that:

State-machine transition
+
Database update
+
Kafka publish

automatically form one atomic transaction.

They don’t.

25. State machine with Kafka or RabbitMQ

A common architecture looks like this:

REST API
↓
Payment Service
↓
State Machine
↓
State Transition
↓
Domain Event
↓
Kafka / RabbitMQ

For example:

AUTH_SUCCESS
↓
State Machine
↓
AUTHORIZED
↓
PaymentAuthorized
↓
Kafka

Other services can consume the domain event:

Kafka
|
+-----------+-----------+
| | |
↓ ↓ ↓
Fraud Audit Notification

This fits naturally with event-driven payment architectures.

26. State-machine events vs domain events

These two concepts are easy to mix up.

State-machine event

AUTH_SUCCESS

This is input to the state machine.

It tells the machine:

“Authorization succeeded.”

Domain event

PaymentAuthorized

This represents something meaningful that happened in the business domain.

A typical flow can therefore be:

AUTH_SUCCESS
↓
State Machine
↓
AUTHORIZED
↓
PaymentAuthorized
↓
Kafka

The first event drives the state transition.

The second event informs other parts of the system about the resulting business change.

27. State transition tables

Before writing the Java configuration, create a transition table.

For example:

Current State

Event

Next State

CREATED

SUBMIT

PENDING

PENDING

START_AUTHORIZATION

AUTHORIZING

PENDING

CANCEL

CANCELLED

AUTHORIZING

AUTH_SUCCESS

AUTHORIZED

AUTHORIZING

AUTH_FAILED

FAILED

AUTHORIZED

START_PROCESSING

PROCESSING

PROCESSING

PROCESS_SUCCESS

COMPLETED

PROCESSING

PROCESS_FAILED

FAILED

This table is more than documentation.

It is the business model you’re implementing.

Once the table is correct, writing the Java configuration becomes much easier.

28. Testing the state machine

Each transition should have a test.

For example:

@Test
void shouldMoveCreatedToPending() {
    // Given
// state machine starts in CREATED
// When
machine.sendEvent(PaymentEvent.SUBMIT);
// Then
assertEquals(
PaymentState.PENDING,
machine.getState().getId()
);
}

You should also test invalid transitions.

For example:

CREATED + PROCESS_SUCCESS

should not produce:

COMPLETED

Likewise:

PENDING + PROCESS_SUCCESS

should not unexpectedly move the payment forward.

Testing illegal transitions is just as important as testing successful ones.

29. Conditional transitions

Now imagine that authorization can have different outcomes.

AUTHORIZING
|
decision
/ \
/ \
approved risk
↓ ↓
AUTHORIZED REVIEW

The decision can be represented through guards or other state-machine mechanisms.

For example:

riskScore < threshold
↓
AUTHORIZED

Otherwise:

riskScore >= threshold
↓
REVIEW

This is where an FSM starts becoming much more useful than a collection of status strings.

30. Hierarchical states

Large state machines can become difficult to manage if every detail is represented at the same level.

Imagine:

PAYMENT_PROCESSING

contains different processing flows:

PAYMENT_PROCESSING
|
+-- CARD
| |
| +-- VALIDATING
| +-- AUTHORIZING
|
+-- UPI
| |
| +-- VALIDATING
| +-- AUTHORIZING
|
+-- BANK
|
+-- VALIDATING
+-- PROCESSING

This is the idea behind hierarchical state machines.

Instead of putting every possible state into one huge flat graph, related states can be grouped together.

31. FSM vs workflow engine

These concepts overlap, but they solve somewhat different problems.

A state machine is naturally suited to an entity lifecycle:

Payment
↓
CREATED
↓
AUTHORIZED
↓
COMPLETED

A workflow is often concerned with coordinating a longer sequence of activities:

Receive payment
↓
Fraud check
↓
Manual approval
↓
Settlement
↓
Notification
↓
Reconciliation

A useful distinction is:

FSM
→ What state is this entity in?
→ Which transitions are allowed?
Workflow
→ Which business activities need to happen?
→ In what order?

A real system can use both.

32. FSM vs Spring Batch

Spring Batch and a state machine solve different problems.

Spring Batch is designed for processing large amounts of data reliably.

For example:

Settlement Batch
↓
Read 1 million payments
↓
Process payments
↓
Write results

A state machine manages lifecycle transitions:

PENDING
↓
AUTHORIZED
↓
COMPLETED

They can still work together.

For example:

Settlement Batch
↓
Read payments
↓
For each payment
↓
State Machine
↓
Validate transition
↓
Update state

33. FSM vs Kafka

Kafka is a messaging platform.

A state machine is a business-state model.

For example:

Kafka
|
| AUTH_SUCCESS
↓
Payment Service
|
↓
State Machine
|
↓
AUTHORIZED

Kafka transports the event.

The state machine determines whether that event is valid for the payment’s current state and what should happen next.

They complement each other rather than replacing each other.

34. Be careful with ownership

A dangerous architecture is one where multiple services can independently modify the same payment lifecycle.

For example:

Kafka
↓
Service A
↓
State Machine
Kafka
↓
Service B
↓
State Machine
Kafka
↓
Service C
↓
State Machine

Who actually owns the payment state?

That question should have a clear answer.

A cleaner model might be:

Payment Service
|
+-- owns Payment state
|
+-- owns state machine
|
+-- publishes domain events

Other services consume those events without directly changing the payment’s lifecycle.

This becomes particularly important once multiple services and asynchronous messaging are involved.

35. Retry and recovery states

Real payment systems have transient failures.

A simple model might be:

AUTHORIZING
|
| failure
↓
FAILED

But a timeout doesn’t always mean the payment has permanently failed.

You might instead introduce:

AUTHORIZING
|
| timeout
↓
RETRY_PENDING
|
↓
AUTHORIZING

You could keep:

attemptCount = 1
attemptCount = 2
attemptCount = 3

in persistent state or context.

After the maximum number of attempts:

AUTHORIZING
↓
RETRY_PENDING
↓
maximum retries reached
↓
FAILED

This is closer to what you’d encounter in a production payment flow.

36. Terminal states

Some states should have no normal outgoing transitions.

For example:

COMPLETED
CANCELLED

Once a payment is completed, you probably don’t want it silently moving back into:

PENDING

A terminal state can therefore be modeled without outgoing transitions:

COMPLETED
|
+--- no outgoing transitions

Whether FAILED is terminal depends on the business design. Some systems allow a failed payment to be retried, while others create a new payment attempt instead.

37. State-machine invariants

A useful way to design the model is to define what each state guarantees.

For example:

COMPLETED

Could mean:

Payment processing succeeded.

Therefore:

COMPLETED → PENDING

would violate that meaning.

CANCELLED

Could mean:

Payment will no longer be processed.

Therefore:

CANCELLED → PROCESSING

would also violate the business rule.

These are invariants.

The state machine becomes one mechanism for enforcing those lifecycle rules.

38. Concurrency: a senior-level concern

Now we get to one of the most important production problems.

Suppose:

Current state = PENDING

Two events arrive almost simultaneously:

AUTH_SUCCESS
CANCEL

You could potentially have:

+--> AUTHORIZED
/
PENDING ----
\
+--> CANCELLED

Which one should win?

A state machine library alone doesn’t magically solve distributed concurrency.

Your application needs a strategy.

Possible approaches include:

  • serializing events for a payment
  • optimistic locking
  • database version columns
  • partitioning Kafka messages by payment ID
  • controlled synchronization
  • single-threaded processing per aggregate
  • database-level constraints

For distributed payment systems, this question can be more important than the choice of state-machine framework.

39. Optimistic locking

Consider a payment table:

CREATE TABLE payment (
id BIGINT PRIMARY KEY,
status VARCHAR(30),
version BIGINT
);

Suppose:

TX100
status = PENDING
version = 5

Two threads read it.

Both see:

version = 5

Thread A processes:

AUTH_SUCCESS

and updates:

PENDING → AUTHORIZED
version 5 → 6

Thread B then tries:

PENDING → CANCELLED

but it is still working with version 5.

The database can reject the stale update because the version is no longer 5.

This gives you protection against concurrent updates.

40. A state machine isn’t a replacement for database consistency

It’s possible for your in-memory state machine to say:

AUTHORIZED

while another piece of code changes the database to:

CANCELLED

unless the architecture prevents that.

That’s why you need clear ownership and persistence rules.

The state machine should be part of the consistency design, not the entire consistency mechanism.

41. How to design an FSM from scratch

When you’re given a new business problem, resist the temptation to start writing Java immediately.

Start with the domain.

Step 1 — Identify the entity

For our example:

Payment

Step 2 — Identify meaningful business states

CREATED
PENDING
AUTHORIZED
PROCESSING
COMPLETED
FAILED
CANCELLED

Step 3 — Identify events

SUBMIT
AUTHORIZE
AUTH_SUCCESS
AUTH_FAILED
CANCEL
PROCESS
COMPLETE

Step 4 — Build the transition table

Current State | Event | Next State

Step 5 — Identify guards

Ask:

What conditions must be true?

Step 6 — Identify actions

Ask:

What side effects happen?

Step 7 — Identify terminal states

For example:

COMPLETED
CANCELLED

Step 8 — Define recovery paths

Think about:

timeout
retry
manual review
compensation

Step 9 — Define persistence

Decide:

Where is the authoritative state stored?

Step 10 — Define concurrency

Ask:

What happens if two events arrive at almost the same time?

Only after answering these questions should you move into the framework configuration.

42. A practical learning path

If you’re learning Spring Statemachine from scratch, I’d go in this order:

Level 1
FSM fundamentals
↓
Level 2
States and Events
↓
Level 3
Transitions
↓
Level 4
Actions
↓
Level 5
Guards
↓
Level 6
Extended State
↓
Level 7
Listeners
↓
Level 8
Conditional transitions
↓
Level 9
Hierarchical states
↓
Level 10
Persistence
↓
Level 11
Recovery and restart
↓
Level 12
Concurrency
↓
Level 13
Kafka/RabbitMQ integration
↓
Level 14
Outbox and transaction boundaries
↓
Level 15
Production architecture

At that point, you’re no longer just learning an API. You’re learning how to model business lifecycles.

43. Build a real project

Rather than starting with something like a traffic-light example, build a payment lifecycle state machine.

Start with:

CREATED
↓
PENDING
↓
AUTHORIZING
↓
AUTHORIZED
↓
PROCESSING
↓
COMPLETED

Then introduce failure paths:

AUTHORIZING → FAILED
PROCESSING → FAILED
PENDING → CANCELLED

After that, add:

  1. PostgreSQL
  2. Spring Data JPA
  3. REST APIs
  4. Kafka or RabbitMQ
  5. Optimistic locking
  6. Retry handling
  7. Audit records
  8. Outbox pattern
  9. Metrics
  10. Transition tests

Once you add those pieces, you’ll start seeing how an FSM fits into a real Spring Boot application rather than existing as an isolated framework exercise.

44. The concepts you should remember

At the end of the day, these terms should be very clear.

State

Where is the entity right now?

AUTHORIZED

Event

What happened?

AUTH_SUCCESS

Transition

How can the entity move?

AUTHORIZING + AUTH_SUCCESS
↓
AUTHORIZED

Guard

Is the transition allowed?

amount <= availableLimit

Action

What work should happen when the transition occurs?

save authorization
publish event
write audit

Extended State

What additional context does the machine need?

attemptCount
riskScore
paymentId

Persistence

How does the state survive an application restart?

PostgreSQL

Listener

How can other components observe state-machine activity?

state changed
transition completed
event rejected

And the whole idea can be reduced to one flow:

Current State
+
Event
|
↓
Guard
|
+------+------+
| |
false true
| |
No transition Transition
|
↓
Next State
|
↓
Action

That’s the foundation.

Once this mental model is solid, Spring Statemachine becomes much easier to understand because you’re no longer learning a collection of framework methods. You’re mapping a well-defined business model onto Java.

--

--

Manish Pandey
Manish Pandey