Practical guide
How to automate order processing
Drive an order with an explicit workflow and hand slow side effects to reliable workers.
In short
State is a fact; automation is a reaction
An order has a small number of allowed states and transitions. Email, ERP export, or shipment creation are reactions to an event, not more ambiguous values in a status column.
Join the data change and event record in one database transaction. A queue can then process messages safely even after a temporary external outage.
Prepare
Describe the process before the code
Automation without an owner and clear boundaries becomes a script that changes an order from several places at once.
- A diagram of states, allowed transitions, conditions, and who may trigger them.
- A list of side effects: payment, inventory, shipping, documents, ERP, and customer communication.
- Unique IDs for orders, events, and processing attempts.
- A queue with acknowledgements, retries, a dead-letter queue, and worker monitoring.
Steps 1 to 3
Separate the decision from its execution
Decide and persist the new state synchronously. Complete anything slow or failure-prone asynchronously.
1. Create a state machine
- Use states such as new, awaiting_payment, paid, picking, shipped, and cancelled; define each one as a business rule.
- Every transition has guards. Paid requires confirmed payment, shipped requires a shipment, and cancelled respects items already dispatched.
- One application use case performs transitions. Controllers, cron jobs, and workers must not overwrite state directly.
- Store change history separately with the old and new state, reason, actor, and time.
php bin/console workflow:dump order Official Symfony Workflow documentation 2. Persist the change and event atomically
- After a successful transition, create an event named as a past-tense fact, such as OrderPaid or OrderCancelled.
- The payload carries an event ID, order ID, schema version, occurred_at, and only the data consumers need. Do not serialize an ORM entity into it.
- Write the order and event to an outbox table in one transaction. A publisher subsequently sends committed rows to the queue.
- An optimistic lock or unique business key prevents two concurrent requests from performing the same transition twice.
BEGIN; UPDATE orders ...; INSERT INTO outbox ...; COMMIT; Official PostgreSQL transaction documentation 3. Run idempotent workers
- The worker first checks the event ID or business key. A redelivery succeeds without sending a second email, shipment, or invoice.
- Acknowledge only after committing the local record. Retry transient errors with exponential backoff and jitter.
- After a bounded number of attempts, move permanent failures to a dead-letter queue with useful context but no secrets.
- Give operators an exception path: they can correct input, retry safely, and see a complete audit trail.
php bin/console messenger:consume async --time-limit=3600 Official Symfony Messenger documentation Step 4
Simulate duplicates and outages
The process is reliable when retries, restarts, and concurrency still produce the same result.
-
Deliver the event twice
The worker performs the side effect once and marks the second delivery as already processed.
php bin/phpunit --filter OrderWorkflowIdempotency -
Stop a worker after the external call
After restart it does not duplicate a shipment or document; it uses the external idempotency key or a local result mapping.
-
Trigger two changes concurrently
One succeeds and the other gets a readable conflict. History and outbox match only the committed transition.
Troubleshooting
Common problems
The order is paid but its event was not sent
The data write and publish are not atomic. Use a transactional outbox and a separate retryable publisher.
The customer received the same email twice
The consumer assumes exactly-once delivery. Add an idempotency key and a persistent record of the completed effect.
A message retries forever
Distinguish transient from permanent errors, cap retries, and move exhausted messages to the DLQ with an alert.
Both a controller and a worker change state
Move the transition into one application use case and route every entry point through it.
Done
The order now has a controllable workflow.
Transitions are explicit, events do not disappear, and workers handle redelivery and service outages safely.