Shopware 6 Order State Machine: Architecture Note for Reliable Order Flow
Design and validate the Shopware 6 order state machine with deterministic transitions, audit logging, and clear failure modes.
19 Jul 2025, 08:45 UTC

Problem: unvalidated order state changes can break audit and fulfillment
In Shopware 6, an order’s current state is stored on the order entity. If payment callbacks, admin actions, or custom plugins write that value directly, the order can enter a state with no recorded transition, no audit entry, and no way to replay what happened. The practical takeaway is to route every state change through a single validation point, log the change, and treat the state machine configuration as read-only code.
Requirements
- Deterministic transitions: a given state plus event yields exactly one next state.
- Audit trail: every change records previous state, new state, transition name, timestamp, and actor.
- Extensibility: custom states can be added without editing core files.
- Payment integration: external provider events trigger transitions through the same service.
- Integrity: only the state machine service may persist
state_id.
Smallest suitable design
Use four parts:
- Order entity – authoritative
state_idand optionalstate_machine_name. - State machine service – loads definitions, validates the requested transition, persists the new state, and dispatches an event.
- Event bus – emits an order-state-changed event so listeners can send email or update external systems without blocking the transition.
- Read-only YAML configuration – transition definitions deployed with code, not edited at runtime.
Example YAML definition
Verify the exact key names and nesting against your Shopware 6 minor version; syntax can change between releases.
# config/packages/shopware/state_machine.yaml (illustrative)
state_machine:
order:
initial: created
states:
created:
type: initial
paid:
type: normal
canceled:
type: terminal
transitions:
pay:
from: created
to: paid
cancel:
from: created
to: canceled
Illustrative service skeleton
This PHP skeleton shows responsibilities only; method names such as StateMachineRegistry::get must be confirmed against your installed Shopware 6 version.
orderRepository->search(
new Criteria([$orderId]),
$context
)->first();
if (!$order) {
throw new \InvalidArgumentException('Order not found');
}
$stateMachine = $this->stateMachineRegistry->get('order', $order, $context);
$currentState = $stateMachine->getState();
$transition = $stateMachine->getTransition($transitionName);
if ($transition === null || $transition->getFromState() !== $currentState) {
throw new \RuntimeException(sprintf(
'Invalid transition %s from %s',
$transitionName,
$currentState
));
}
$this->orderRepository->update([
[
'id' => $orderId,
'stateId' => $transition->getToState()->getId(),
],
], $context);
$this->eventDispatcher->dispatch(new OrderStateChangedEvent(
$orderId,
$transitionName,
$currentState,
$transition->getToState()->getName()
));
}
}
Trust and data boundaries
- The order entity holds the authoritative state. Other services must not write
state_iddirectly. - YAML configuration is read-only at runtime; changes require a deployment.
- Event listeners receive event data and must not mutate the order state directly.
- The state machine service should not write to unrelated entities such as inventory; side effects belong in dedicated listeners.
Operational checks
- Pre-transition validation – confirm the transition exists and matches the current state before writing.
- Audit logging – write each change to an audit table with columns
order_id,previous_state,new_state,transition_name,changed_at, andchanged_by. - Event emission – dispatch
OrderStateChangedEventafter the database write succeeds. - Unit tests per path – test every defined transition and assert that invalid transitions throw an exception.
- Configuration health check – verify the registry loads all state machines and no referenced state is missing.
Command example: confirm audit table exists
Run this on a non-production database first, as the application user with database access.
# Run from the Shopware project root
bin/console doctrine:schema:update --dump-sql
Expected check: the SQL output contains a CREATE TABLE statement for your audit table. Risk: running without --dump-sql can alter production schema.
Command example: inspect current order state
# Run as a database user with read access
SELECT id, state_id FROM order WHERE id = '';
Expected check: state_id matches the state you expect after the last transition. Risk: direct SQL reads bypass the application layer and may return uncommitted data depending on isolation level.
Failure modes
- Invalid transition exception – thrown when the requested transition is not allowed from the current state.
- Concurrency race – two processes transition the same order at once; without optimistic locking, one update can overwrite the other. Mitigate with a
versioncolumn or row lock. - Missing event handlers – a listener such as payment confirmation may be absent, leaving an order in an intermediate state.
- Configuration drift – YAML edited on one server but not deployed everywhere causes inconsistent transitions.
- Direct database writes – bypassing the service breaks the audit trail and can lock orders in terminal states.
Conditions that would change the design
- More than about ten custom states or twenty transitions per state may justify database-backed configuration instead of static YAML.
- Cross-domain workflows with logistics or subscriptions may need an event-driven saga rather than a single order state machine.
- High transition volume may require optimistic locking and queued state changes.
- New regulatory audit fields may require a separate immutable audit store.
- Synchronization with an external partner state machine requires a bidirectional sync layer.
Limitations and verification
- State machine definitions are version-dependent; older YAML syntax may break on newer Shopware releases.
- Custom state extensions require careful mapping to avoid breaking default workflows.
- Practical check: run a unit test that attempts an invalid transition and asserts an exception is thrown.
- Practical check: simulate two concurrent transitions on the same order and verify the final state matches the last committed transition.
- Practical check: inspect the audit table for entries whose
changed_attimestamps match transition times.
Conclusion
A lightweight service, read-only YAML config, and strict boundary around the order entity keep Shopware 6 order flow deterministic and auditable. Monitor concurrency and audit completeness, and redesign when custom workflows or volume outgrow the minimal design.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.