Practical guide

How to automate order processing

Drive an order with an explicit workflow and hand slow side effects to reliable workers.

25 minutes · Workflows and queues

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

  1. Use states such as new, awaiting_payment, paid, picking, shipped, and cancelled; define each one as a business rule.
  2. Every transition has guards. Paid requires confirmed payment, shipped requires a shipment, and cancelled respects items already dispatched.
  3. One application use case performs transitions. Controllers, cron jobs, and workers must not overwrite state directly.
  4. 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

  1. After a successful transition, create an event named as a past-tense fact, such as OrderPaid or OrderCancelled.
  2. 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.
  3. Write the order and event to an outbox table in one transaction. A publisher subsequently sends committed rows to the queue.
  4. 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

  1. The worker first checks the event ID or business key. A redelivery succeeds without sending a second email, shipment, or invoice.
  2. Acknowledge only after committing the local record. Retry transient errors with exponential backoff and jitter.
  3. After a bounded number of attempts, move permanent failures to a dead-letter queue with useful context but no secrets.
  4. 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.

  1. Deliver the event twice

    The worker performs the side effect once and marks the second delivery as already processed.

    php bin/phpunit --filter OrderWorkflowIdempotency
  2. 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.

  3. 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.

Request a call

I will call you on the next working day between 9:00 and 17:00.

You can also call me directly.

+420 605 181 728

Leave your phone number and send a callback request.

By sending, you agree to processing your data in order to handle your request.