15 minlesson

State Machine Concepts for Parcel Lifecycle

State Machine Concepts for Parcel Lifecycle

A parcel moves through a series of statuses from creation to delivery. Not every transition makes sense --- a delivered parcel cannot suddenly become "in transit" again. In the previous topic, we built tracking event creation with basic status synchronization: when an event is added, the parcel status updates automatically. But that implementation allows any transition. A state machine formalizes the rules so your API can enforce them consistently and reject invalid transitions.

What Is a State Machine?

A state machine is a model that defines:

  • A finite set of states an entity can be in
  • A set of transitions that move the entity from one state to another
  • Rules that determine which transitions are valid from each state
1┌──────────────┐ ┌──────────┐ ┌───────────┐ ┌────────────────┐ ┌───────────┐
2│ LabelCreated │────>│ PickedUp │────>│ InTransit │────>│ OutForDelivery │────>│ Delivered │
3└──────────────┘ └──────────┘ └───────────┘ └────────────────┘ └───────────┘
4 │ │ │ │
5 │ │ │ │
6 v v v v
7 ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐
8 │ Exception │ │ Exception │ │ Exception │ │ Exception │
9 └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘
10 │ │ │ │
11 v v v v
12 ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
13 │ Returned │ │ Returned │ │ Returned │ │ Returned │
14 └──────────┘ └──────────┘ └──────────┘ └──────────┘

Parcel Statuses

Our parcel tracking system uses these statuses:

StatusDescriptionTerminal?
LabelCreatedShipping label generated, parcel not yet handed offNo
PickedUpCarrier has collected the parcelNo
InTransitParcel is moving through the carrier networkNo
OutForDeliveryParcel is on the delivery vehicleNo
DeliveredParcel successfully delivered to recipientYes
ExceptionSomething went wrong (damage, address issue, customs hold)No
ReturnedParcel sent back to senderYes

Terminal states mean the parcel's journey is over. No further transitions are allowed.

Valid Transitions

The happy path follows a linear progression:

LabelCreated -> PickedUp -> InTransit -> OutForDelivery -> Delivered

Beyond the happy path, two additional rules apply:

  1. Exception can be reached from any non-terminal state. Problems can occur at any point in the journey.
  2. Returned can be reached from Exception. Once an exception is resolved by returning the parcel, it becomes terminal.

Here is the complete transition table:

From StateAllowed Next States
LabelCreatedPickedUp, Exception
PickedUpInTransit, Exception
InTransitOutForDelivery, Exception
OutForDeliveryDelivered, Exception
ExceptionReturned
Delivered(none -- terminal)
Returned(none -- terminal)

Why Not Allow Arbitrary Transitions?

Without a state machine, you could end up with nonsensical data:

  • A parcel marked Delivered that later becomes LabelCreated
  • A parcel jumping from LabelCreated directly to Delivered with no tracking history
  • A Returned parcel suddenly going OutForDelivery

These corrupt the audit trail and confuse both internal systems and customers looking at tracking information.

Representing States in C#

An enum is the natural choice for a fixed set of states:

csharp
1public enum ParcelStatus
2{
3 LabelCreated,
4 PickedUp,
5 InTransit,
6 OutForDelivery,
7 Delivered,
8 Exception,
9 Returned
10}

The allowed transitions map naturally to a dictionary:

csharp
1private static readonly Dictionary<ParcelStatus, HashSet<ParcelStatus>> AllowedTransitions = new()
2{
3 [ParcelStatus.LabelCreated] = [ParcelStatus.PickedUp, ParcelStatus.Exception],
4 [ParcelStatus.PickedUp] = [ParcelStatus.InTransit, ParcelStatus.Exception],
5 [ParcelStatus.InTransit] = [ParcelStatus.OutForDelivery, ParcelStatus.Exception],
6 [ParcelStatus.OutForDelivery] = [ParcelStatus.Delivered, ParcelStatus.Exception],
7 [ParcelStatus.Exception] = [ParcelStatus.Returned],
8 [ParcelStatus.Delivered] = [],
9 [ParcelStatus.Returned] = [],
10};

An empty set means no transitions are allowed -- the state is terminal.

Service Layer Separation

Business rules like status validation belong in a service layer, not in the controller. This separation provides several benefits:

  • Testability: You can unit test the transition logic without HTTP concerns.
  • Reusability: Multiple endpoints or background processes can share the same rules.
  • Clarity: Controllers handle HTTP; services handle domain logic.
1┌────────────┐ ┌─────────────────────┐ ┌────────────┐
2│ Controller │────>│ ParcelStatusService │────>│ Repository │
3│ (HTTP) │ │ (business rules) │ │ (data) │
4└────────────┘ └─────────────────────┘ └────────────┘

The controller receives the request, calls the service to validate and execute the transition, and returns the appropriate HTTP response based on the result.

HTTP Status Codes for Business Rule Violations

When a client requests an invalid status transition, the API should return 422 Unprocessable Entity. This status code means:

  • The request was well-formed (not a 400)
  • The server understood it (not a 500)
  • But it cannot process it because it violates business rules
1Client: PATCH /api/parcels/123
2 Content-Type: application/json-patch+json
3 [{ "op": "replace", "path": "/status", "value": "Delivered" }]
4 (parcel is currently LabelCreated)
5
6Server: 422 Unprocessable Entity
7 {
8 "error": "invalid_transition",
9 "currentStatus": "LabelCreated",
10 "requestedStatus": "Delivered",
11 "allowedStatuses": ["PickedUp", "Exception"]
12 }

For terminal states, attempting any modification should also return 422 with a clear message explaining that the parcel is in a terminal state and cannot be modified.

Key Takeaways

  • A state machine defines valid states and transitions for an entity
  • Parcel status follows a linear happy path with Exception branching from any active state
  • Delivered and Returned are terminal states -- no further changes allowed
  • Use a dictionary of allowed transitions to implement the state machine in C#
  • Business rules belong in a service layer, separate from controllers
  • Return 422 Unprocessable Entity for invalid status transitions