Shopware ERP Order Export: Asynchronous Message Queue Architecture
Keep Shopware checkout independent of ERP availability by dispatching order exports through Symfony Messenger with a custom handler, retry policies, and strict data boundaries.
25 Aug 2026, 08:47 UTC

The problem: ERP latency inside checkout
When an order export calls an external ERP REST API synchronously during Shopware checkout, every millisecond of ERP latency becomes checkout latency. If the ERP is slow, rate-limited, or down, the customer sees a timeout or a failed order, even though Shopware itself is healthy. The useful takeaway is that Shopware's built-in Symfony Messenger integration lets you move the export out of the request path with a small plugin, keeping checkout responsive while making the export retryable and observable.
Requirements
Before choosing a design, fix the boundaries the solution must satisfy:
- Checkout latency must be independent of ERP availability.
- The export must be retryable, because the ERP API may be slow, rate-limited, or temporarily down.
- The handler must be idempotent, so a retry after a partial failure does not create duplicate orders in the ERP.
- No customer PII or API credentials may travel through the message broker.
- Operators need visibility into queue depth, retry counts, and permanently failed messages.
Smallest suitable design
The smallest design that meets these requirements uses Shopware's Symfony Messenger integration with the doctrine transport, which stores messages in a database table. If RabbitMQ or another broker is already deployed, use that transport instead. Add a plugin that dispatches a message when an order is placed and implements a MessageHandler that calls the ERP REST API. Avoid a separate microservice initially; the added operational surface is not justified until throughput or ordering demands it.
A minimal message carries only the order identifier:
final class OrderExportMessage
{
public function __construct(
public readonly string $orderId
) {}
}
The handler loads the order from the Shopware repository using that ID and calls the ERP. Credentials are read from encrypted system configuration, never embedded in the message:
final class ErpExportHandler implements MessageHandlerInterface
{
public function __invoke(OrderExportMessage $message): void
{
// Load order entity via $message->orderId
// Read ERP credentials from encrypted SystemConfigService
// POST to ERP REST endpoint
}
}
Routing is declared in messenger.yaml. The exact key names and DSN format are version-sensitive in Shopware 6; confirm the schema against your installed Symfony Messenger version before deploying:
framework:
messenger:
transports:
erp_export: '%env(MESSENGER_TRANSPORT_DSN)%'
routing:
'App\Message\OrderExportMessage': erp_export
Run the worker from the Shopware root directory as the web server or a dedicated system user, supervised by systemd or Supervisor so it restarts after a crash:
bin/console messenger:consume erp_export --limit=100
Trust and data boundaries
Shopware core and the export plugin form the trusted boundary for order data. The ERP is external and untrusted. Because messages are serialized and may persist in a database table or broker queue that is less protected than the Shopware database, the payload must contain only the order ID. If the broker is compromised, an attacker sees an opaque identifier, not names, addresses, or payment details. API credentials belong in Shopware's encrypted system configuration, not in the message, the environment file, or the handler's source code.
Operational checks
Asynchronous export fails silently unless you watch it. Monitor the messenger_messages table (or the RabbitMQ queue) for depth, and track the count of messages in retry or failed states. Alert when messages remain stuck beyond a defined age, for example older than one hour. Log the ERP response code for every outbound request so you can audit which exports succeeded and which were rejected.
To inspect failures, use the Messenger CLI tools from the Shopware root:
bin/console messenger:failed:show
This lists messages that exhausted retries. To replay a specific message after fixing the underlying issue, note its ID and run:
bin/console messenger:failed:retry <message_id>
Expected checks in a development environment: place a test order and confirm a row appears in the transport table and that the handler logs the outbound ERP request. Stop the mock ERP endpoint and verify the message is retried according to the configured policy, then lands in the failed transport. Inspect a serialized message to ensure it contains only the order ID and no customer PII or credentials.
Failure modes
Several failure modes are specific to this pattern:
- ERP outage exhausts retries. If the ERP stays down longer than the retry window, messages dead-letter. They will not retry automatically; an operator must inspect and replay them.
- Non-idempotent handlers. If the ERP accepts a request but the response is lost, a retry can create a duplicate order. The handler must use an idempotency key or check-before-create logic.
- Serialization breakage. Changing the message class name or constructor signature after messages are queued can break deserialization and strand the queue. Treat the message class as a published contract.
- Worker crash. If the consumer process dies and is not supervised, the queue stops draining. A database-backed backlog can grow unnoticed and eventually impact database size and performance.
Conditions that would change the design
This design is appropriate for moderate order volume and fire-and-forget export. Two conditions warrant a different approach:
- Throughput exceeds a single worker, or strict ordering is required. Move to a dedicated broker such as RabbitMQ with multiple consumers and partition by order ID if ordering matters.
- The ERP needs synchronous confirmation. If the ERP must confirm receipt before checkout completes, replace fire-and-forget with an outbox or saga pattern and compensating actions.
Limitations and practical verification
The doctrine transport keeps messages in the Shopware database. Without a separate worker and monitoring, a backlog can grow unnoticed and consume storage. Always confirm the available transports, routing keys, and retry options against your installed Shopware version and the Symfony Messenger documentation, because these are version-sensitive. The practical check is to run the full cycle in a staging environment: dispatch, consume, fail, inspect, and retry. If the serialized payload contains only the order ID and the failed-message tools work as expected, the boundary and operational model are sound.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.