{"id":263,"date":"2026-03-20T10:36:03","date_gmt":"2026-03-20T10:36:03","guid":{"rendered":"https:\/\/magendoo.ro\/insights\/integration-middleware-101-dto-mapping-retries-and-dead-letter-queues\/"},"modified":"2026-03-20T10:36:03","modified_gmt":"2026-03-20T10:36:03","slug":"integration-middleware-101-dto-mapping-retries-and-dead-letter-queues","status":"publish","type":"post","link":"https:\/\/magendoo.ro\/insights\/integration-middleware-101-dto-mapping-retries-and-dead-letter-queues\/","title":{"rendered":"Integration Middleware 101: DTO Mapping, Retries, and Dead-Letter Queues"},"content":{"rendered":"<p>Export raw data from Magento. Transform it somewhere else. This is the pattern.<\/p>\n<p>Every Magento integration starts the same way. Someone writes a module that reads order data, reformats it into the ERP\u2019s expected structure, and pushes it via API \u2014 all inside a cron job. It works for the first integration. By the third, you have a transformation layer, a retry mechanism, and error handling logic buried inside Magento\u2019s runtime. You didn\u2019t plan to build middleware. But that\u2019s what you built \u2014 bad middleware, running inside your commerce engine.<\/p>\n<p>There\u2019s a cleaner approach, and it starts with accepting one thing: Magento should export raw data. Everything else \u2014 mapping, retries, failure handling \u2014 belongs in a dedicated middleware layer.<\/p>\n<h2 class=\"wp-block-heading\">Why Integration Logic Ends Up in Magento<\/h2>\n<p>It\u2019s the same story every time. A new integration requirement comes in \u2014 push orders to SAP, sync inventory from a WMS, send shipment confirmations to a 3PL. The team looks at what they have: Magento\u2019s cron, its queue consumers, its DI container. Why introduce another service when you can write a module?<\/p>\n<p>Here\u2019s the trap:<\/p>\n<ul>\n<li><strong>The first integration is always simple.<\/strong> One cron job, one API call, one format. It fits neatly in a Magento module.<\/li>\n<li><strong>The second integration copies the pattern.<\/strong> Now you have two cron jobs with similar-but-different transformation logic.<\/li>\n<li><strong>By the fifth, you have an unmaintainable mess.<\/strong> Retry logic is entangled with DTO mapping. Error states are logged to <code>var\/log\/<\/code> with no alerting. Failed records get silently skipped because nobody built a dead-letter queue inside a Magento module.<\/li>\n<\/ul>\n<p>The real problem isn\u2019t technical \u2014 it\u2019s organizational. Agencies scope Magento hours. Nobody budgets for middleware. So transformation logic grows inside the platform like mold behind drywall.<\/p>\n<h2 class=\"wp-block-heading\">The Pattern: Export Raw, Transform Outside<\/h2>\n<p>The architecture is straightforward:<\/p>\n<p><strong>Magento exports raw commerce data.<\/strong> Orders, prices, inventory levels, customer updates \u2014 whatever the integration needs. The format is Magento\u2019s native structure. No transformation. No ERP-specific fields. Just clean commerce payloads pushed to a queue or exposed via API.<\/p>\n<p><strong>Middleware receives, maps, and delivers.<\/strong> A Go service (or any dedicated middleware) picks up the raw payload, transforms it into the target system\u2019s DTO, and pushes it to the ERP, WMS, or 3PL. This is where field mapping lives. This is where business rules for transformation live. And this is where retry logic belongs.<\/p>\n<p>This separation has three immediate benefits:<\/p>\n<ul>\n<li><strong>Magento stays focused on commerce.<\/strong> No SAP field mappings polluting your order export module. No ERP connection timeouts blocking your cron runner.<\/li>\n<li><strong>Transformation logic is testable in isolation.<\/strong> You can unit test a DTO mapping function without bootstrapping Magento\u2019s DI container.<\/li>\n<li><strong>Each integration is independent.<\/strong> SAP\u2019s retry policy doesn\u2019t affect your shipping provider integration. Failures are isolated.<\/li>\n<\/ul>\n<h2 class=\"wp-block-heading\">DTO Mapping: Keep It Boring<\/h2>\n<p>DTO mapping sounds trivial. It\u2019s not \u2014 at scale.<\/p>\n<p>An order in Magento has nested structures: items, addresses, payment info, custom attributes, extension attributes. An ERP expects a flat document with specific field names, date formats, and enum values that don\u2019t match Magento\u2019s. The mapping between these two is where most integration bugs live.<\/p>\n<p>The rules are simple:<\/p>\n<ul>\n<li><strong>One mapper per integration target.<\/strong> Don\u2019t build a generic \u201ctransform anything\u201d engine. You\u2019ll end up maintaining a DSL nobody understands. Write explicit struct-to-struct mappings.<\/li>\n<li><strong>Validate before sending.<\/strong> After mapping, validate the output DTO against the target system\u2019s requirements. Catch missing fields and format errors before they become API failures.<\/li>\n<li><strong>Version your DTOs.<\/strong> When the ERP updates their API, you change the mapper. The Magento export doesn\u2019t change. The queue format doesn\u2019t change. Only the last mile changes.<\/li>\n<\/ul>\n<p>In Go, this is particularly clean. You define typed structs for both source (Magento payload) and target (ERP document), write a function that maps one to the other, and test it with table-driven tests. No reflection magic. No runtime surprises.<\/p>\n<p>If you\u2019re building Go services that consume Magento\u2019s REST API, <a href=\"https:\/\/github.com\/florinel-chis\/go-m2rest\">go-m2rest<\/a> gives you typed structs and a search criteria builder out of the box \u2014 so you\u2019re not hand-rolling HTTP calls and JSON parsing for every integration.<\/p>\n<h2 class=\"wp-block-heading\">Retries and Backoff: The Non-Negotiable<\/h2>\n<p>External APIs fail. ERP endpoints go down for maintenance. Rate limits get hit during peak sync windows. If your integration doesn\u2019t handle retries properly, you lose data.<\/p>\n<p>But \u201cjust retry\u201d is not a strategy. Here\u2019s what actually works:<\/p>\n<ul>\n<li><strong>Exponential backoff with jitter.<\/strong> Don\u2019t hammer a struggling endpoint with fixed-interval retries. Back off exponentially, add random jitter to prevent thundering herd, and cap the maximum delay.<\/li>\n<li><strong>Retry budget per message.<\/strong> Set a maximum retry count (typically 3-5). After that, the message moves to the dead-letter queue. Don\u2019t retry forever \u2014 you\u2019ll mask the real problem.<\/li>\n<li><strong>Distinguish transient from permanent failures.<\/strong> A 503 is retryable. A 400 with a validation error is not. Retrying a permanent failure wastes time and obscures the root cause.<\/li>\n<li><strong>Make retries idempotent.<\/strong> If you retry pushing an order to the ERP, the ERP must handle receiving the same order twice. This means idempotency keys, deduplication checks, or upsert semantics. Without this, every retry is a potential duplicate.<\/li>\n<\/ul>\n<p>In Magento\u2019s cron model, retry logic becomes a state machine managed in database columns \u2014 <code>retry_count<\/code>, <code>last_attempt<\/code>, <code>status<\/code>. It works until you have 50,000 queued records and your cron job takes 45 minutes to scan the table. In a Go service, retries are part of the message processing loop with in-memory state and configurable policies per consumer.<\/p>\n<h2 class=\"wp-block-heading\">Dead-Letter Queues: Where Failed Messages Go to Be Fixed<\/h2>\n<p>A dead-letter queue (DLQ) is not a graveyard \u2014 it\u2019s a triage room. Messages end up here when they\u2019ve exhausted their retry budget. The DLQ gives you:<\/p>\n<ul>\n<li><strong>Visibility.<\/strong> You know exactly which records failed and why. No more grepping through Magento logs hoping to find the error.<\/li>\n<li><strong>Replay capability.<\/strong> Fix the root cause (bad mapping, API change, missing field), then replay the failed messages. No data loss. No manual re-entry.<\/li>\n<li><strong>Alerting.<\/strong> A message hitting the DLQ triggers an alert. Your team knows something is broken before the business notices missing orders.<\/li>\n<\/ul>\n<p>The pattern in practice: your Go consumer reads from a main queue (RabbitMQ, Kafka, SQS). If processing fails after N retries, it publishes the message to a DLQ topic with metadata \u2014 original timestamp, error message, retry count. A separate dashboard or CLI tool lets you inspect, fix, and replay.<\/p>\n<p>I built this exact pattern into <a href=\"https:\/\/github.com\/florinel-chis\/tracking-updater\">tracking-updater<\/a> \u2014 failed shipment tracking updates move to a failed directory with full context, and can be reprocessed once the issue is resolved.<\/p>\n<h2 class=\"wp-block-heading\">The Magento Side of This<\/h2>\n<p>Magento\u2019s job in this pattern is minimal \u2014 and that\u2019s the point.<\/p>\n<ul>\n<li><strong>Emit events or queue messages<\/strong> when orders are placed, shipments are created, or prices are updated. Use Magento\u2019s native <code>queue_topology.xml<\/code> to publish to RabbitMQ. Keep the message payload as Magento\u2019s native data structure.<\/li>\n<li><strong>Expose REST\/GraphQL endpoints<\/strong> for the middleware to pull data on demand. Let <a href=\"https:\/\/github.com\/florinel-chis\/go-m2rest\">go-m2rest<\/a> handle the API calls with built-in auth and retry.<\/li>\n<li><strong>Don\u2019t transform.<\/strong> Magento doesn\u2019t know or care what format SAP expects. It exports commerce data. Period.<\/li>\n<\/ul>\n<p>This keeps your Magento modules thin, upgrade-safe, and decoupled from every downstream system. When the ERP changes their API, you update the Go middleware. Magento doesn\u2019t get a new deployment.<\/p>\n<h2 class=\"wp-block-heading\">Decision Framework<\/h2>\n<p><strong>Move integration logic to middleware when:<\/strong><\/p>\n<ul>\n<li>You have more than one external system consuming Magento data<\/li>\n<li>Retry and error handling requirements differ per integration<\/li>\n<li>Transformation logic changes independently from commerce logic<\/li>\n<li>You need visibility into failed records beyond Magento logs<\/li>\n<li>Your cron runner is already contended with indexers and other jobs<\/li>\n<\/ul>\n<p><strong>Keep it in Magento when:<\/strong><\/p>\n<ul>\n<li>It\u2019s a single, simple integration with minimal transformation<\/li>\n<li>The team has no capacity to operate a separate service<\/li>\n<li>The data volume is low (hundreds of records, not thousands)<\/li>\n<li>There\u2019s no retry or failure handling requirement beyond \u201clog and move on\u201d<\/li>\n<\/ul>\n<h2 class=\"wp-block-heading\">The Leadership Question<\/h2>\n<p>Middleware sounds like additional infrastructure cost. It is \u2014 initially. But consider the alternative.<\/p>\n<p>Every integration you embed in Magento increases deployment risk. A bug in your SAP mapper can block a Magento release. A stuck cron job can delay indexing. A retry loop can consume PHP workers that should be serving checkout requests.<\/p>\n<p>The real cost isn\u2019t the Go service. It\u2019s the operational coupling. When your integration layer is separate, you can deploy, scale, and debug it independently. Your Magento release pipeline stays clean. Your commerce platform does commerce.<\/p>\n<p>That\u2019s not over-engineering. That\u2019s drawing the right boundary.<\/p>\n<h2 class=\"wp-block-heading\">Conclusion<\/h2>\n<p>The pattern is simple: Magento exports raw data. Middleware transforms, retries, and delivers. Failed messages land in a dead-letter queue where they can be inspected and replayed.<\/p>\n<p>This isn\u2019t a theoretical architecture diagram. It\u2019s what happens when you stop treating Magento as your integration engine and start treating it as your commerce engine. The middleware layer \u2014 whether it\u2019s a Go service, a dedicated integration platform, or even a managed queue with Lambda functions \u2014 is where transformation and reliability belong.<\/p>\n<p>Draw the boundary. Keep Magento focused. Build your middleware to handle the mess that integration always is.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Export raw data from Magento. Transform it somewhere else. This is the pattern. Every Magento integration starts the same way. Someone writes a module that reads order data, reformats it into the ERP\u2019s expected structure, and pushes it via API \u2014 all inside a cron job. It works for the first integration. By the third, [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":262,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"site-container-style":"default","site-container-layout":"default","site-sidebar-layout":"default","disable-article-header":"default","disable-site-header":"default","disable-site-footer":"default","disable-content-area-spacing":"default","footnotes":""},"categories":[1],"tags":[],"class_list":["post-263","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-general"],"_links":{"self":[{"href":"https:\/\/magendoo.ro\/insights\/wp-json\/wp\/v2\/posts\/263","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/magendoo.ro\/insights\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/magendoo.ro\/insights\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/magendoo.ro\/insights\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/magendoo.ro\/insights\/wp-json\/wp\/v2\/comments?post=263"}],"version-history":[{"count":0,"href":"https:\/\/magendoo.ro\/insights\/wp-json\/wp\/v2\/posts\/263\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/magendoo.ro\/insights\/wp-json\/wp\/v2\/media\/262"}],"wp:attachment":[{"href":"https:\/\/magendoo.ro\/insights\/wp-json\/wp\/v2\/media?parent=263"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/magendoo.ro\/insights\/wp-json\/wp\/v2\/categories?post=263"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/magendoo.ro\/insights\/wp-json\/wp\/v2\/tags?post=263"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}