Integration 10 min read

Event-Driven Integration with Symfony Messenger and Redis Streams

CS
CELESDEV Solutions
Integration & Data Practice

The client was a global manufacturing brand managing over 50 web properties across 14 country markets. Product catalogue, pricing, and inventory data originated in SAP S/4HANA and needed to propagate to every market website — historically via synchronous RFC calls from a Symfony middleware layer that had grown brittle over 8 years.

The symptoms were textbook: cascading failures when SAP was slow, inconsistent data states between markets, deployment windows limited to nights and weekends, and a middleware team of six spending 70% of their time on incident response. The solution was a full architectural shift to event-driven integration.

The Core Problem with Synchronous Integration
When your middleware makes synchronous calls to SAP and SAP is slow, your middleware is slow. When your middleware is slow, every downstream website is slow. Synchronous coupling turns one system's performance problem into everyone's incident.

Why Symfony Messenger Over Kafka

This is the question we get asked most. For this client's scale (approximately 15,000 events/hour peak), Kafka would have been architectural over-engineering. Kafka's operational complexity — ZooKeeper/KRaft management, partition design, consumer group rebalancing — would have required dedicated infrastructure expertise the team didn't have.

Redis Streams with Symfony Messenger provided:

The decision rule we apply: if you need Kafka's throughput (millions of events/second), use Kafka. If you need Kafka's semantics at lower volume, Redis Streams is operationally simpler and sufficient.

The Dead-Letter Queue Pattern

The most critical reliability feature in the implementation is the dead-letter queue (DLQ) strategy. Event-driven architectures fail silently when messages are lost. We implemented a three-tier retry and DLQ pattern:

Retry & DLQ ArchitectureEvent Published ↓ [Main Stream] → Consumer → Success ↓ (failure) [Retry Stream x3] → Success ↓ (exhausted) [DLQ Stream] → Alert → Manual Review ↓ (resolved) [Replay] → [Main Stream]

Failed messages move to a retry stream with exponential backoff (30s, 5min, 30min). After three retries, they move to the DLQ. A separate monitoring process alerts the team via PagerDuty when DLQ depth exceeds threshold. Manual review can replay DLQ messages after the root cause is resolved.

SAP Change Data Capture

The upstream feed from SAP was implemented using SAP's Change Document framework rather than polling. An ABAP function module publishes change events to an HTTP endpoint on state changes to relevant business objects (materials, prices, inventory). This push model eliminated the polling overhead and reduced event latency from ~15 minutes (scheduled polling) to under 30 seconds.

A dedicated Symfony "ingestor" service validates, normalises, and enriches incoming SAP events before publishing them to Redis Streams. This separation of concerns means the event schema in the streams is decoupled from SAP's native data structures — an important boundary for maintainability.

Key Takeaways
  • Choose transport technology based on your actual throughput requirements, not hype
  • Redis Streams + Symfony Messenger is production-ready for <100k events/hour
  • Dead-letter queues are non-negotiable — silent message loss is worse than visible failure
  • Decouple your event schema from source system data structures at ingestion time
  • SAP CDC via Change Documents is more reliable than polling-based integration
Related Articles
Integration
Replacing BizTalk with API Platform: A Migration War Story
Read article →
Integration
API Design First: Contract-Driven Development with OpenAPI and AsyncAPI
Read article →
Transformation
Headless CMS at Scale: Governing 50+ Websites Without Losing Your Mind
Read article →