{"componentChunkName":"component---src-templates-post-js","path":"/clean-model-always-wins-strangler-pattern-case-study/","result":{"data":{"markdownRemark":{"html":"<p>This article is a real-life case study of modernizing a legacy PHP application with the Strangler Pattern - migrating a product catalogue from Zend Framework to Symfony, syncing data between the two systems, and the lessons learned from years in production.</p>\n<p><strong>TL;DR</strong></p>\n<ul>\n<li>We strangled a product catalogue out of a legacy Zend Framework app into Symfony. Legacy stayed the only writer, the new app the only reader.</li>\n<li>The entire data sync: database triggers filling a queue table of changed IDs, and a cron job pulling batches every few minutes. No Kafka, no Debezium.</li>\n<li>Reliability came from at-least-once delivery plus an idempotent consumer - implemented with a <code class=\"language-text\">TIMESTAMP</code> column and one guarded <code class=\"language-text\">DELETE</code>.</li>\n<li>The real lesson: focus on a clear domain and clean boundaries, not fancy tools. A clean model with an anti-corruption layer did far more for this project than Kafka or Debezium ever could.</li>\n</ul>\n<hr>\n<p>When implementing a Strangler Pattern approach, one of the key things to master is to keep the new data model clean. More often than not, the model is transferred, or the new model is heavily influenced by the legacy one. As a result, the new system has a newer framework, slightly better codebase, but the deep modelling issues remain, and they make development slow and hard.</p>\n<p>A few weeks ago I published a <a href=\"/blog/strangler-pattern-practical-guide/\">practical guide to the Strangler Pattern</a>. In it I made a claim that deserves proof: when you split the database during a migration, the synchronization between the old and the new store does not have to be sophisticated. More often than you would expect, a delay is perfectly acceptable, and that makes your life much easier.</p>\n<p>This is a case study from one of our long-running projects: a marketplace platform where a legacy Zend Framework application and a new Symfony application ran side by side for years.</p>\n<h2>The background</h2>\n<p>The platform lets shop owners design custom apparel and sell it in their own storefronts. The legacy side is a classic Zend Framework 1 application - thousands of files, models mixed with raw SQL, an admin panel, background jobs, the works. The new side was planned as a Symfony application with a properly modeled domain: bounded contexts for products, shops and currencies, repositories, value objects.</p>\n<h2>Finding the right place to strangle</h2>\n<p>For a migration to succeed, it needs to deliver value to the business. No business owner will be happy to pay for months or years of work without a positive return on investment (ROI). A good place to start needs to combine a positive ROI, low risk, and being rather easy to work on if you are new to the project (and we were).</p>\n<p>After some research, many discussions with the business owners, and a code review, we have chosen to start with a product catalogue page. Some reasons why this was a good candidate:</p>\n<ul>\n<li>The catalogue, especially its search was super slow, even timing out in some cases.</li>\n<li>This is a key step in the buyer journey.</li>\n<li>A catalogue model was quite easy to design...</li>\n<li>... and the sync was easy to implement - as you will see later. </li>\n</ul>\n<p>The important constraint: <strong>the legacy application remains the system of record for products</strong>. Shop owners still create and edit products through the legacy admin. The new application only renders the catalog and the product pages (as a second step), from its own database, with its own clean model. That single decision - one writer, one reader - is what allows the sync to stay simple. Data flows in exactly one direction, and there is no risk of corrupted data. Legacy is always the source of truth (at least at this stage).</p>\n<p>The second decision that keeps things simple: <strong>eventual consistency is fine here</strong>. If a shop owner renames a product and the public catalog shows the old name for three more minutes, nobody notices and nothing breaks. No money is moving. The moment you accept that, you can throw away most of the complicated options.</p>\n<h2>The initial setup</h2>\n<p>So we introduced a new Symfony application that ran next to the old one. The HTTP(S) routing was very simple, and we kicked it off with just a couple of Apache config rules (Apache was already running):</p>\n<div class=\"gatsby-highlight\" data-language=\"apache\"><pre class=\"language-apache\"><code class=\"language-apache\"># New app takes the product pages...\nProxyPass /p       http://new/p\n# ...and the catalogue\nProxyPass /c       http://new/c\n\n# Everything else stays on legacy\nProxyPass /        http://legacy/</code></pre></div>\n<p>With that in place, the new application needed the legacy product data - and after considering different approaches, the whole synchronization mechanism came down to three pieces:</p>\n<ol>\n<li>a <strong>database trigger</strong> that records which products changed,</li>\n<li>a <strong>queue table</strong> holding the IDs of products waiting to be synced,</li>\n<li>a <strong>cron job</strong> on the new side that pulls the changes every few minutes.</li>\n</ol>\n<p>No Kafka, no Debezium, no message broker. And it ran in production, reliably, for years.</p>\n<p>I will walk through the whole pipeline end to end, including the anti-corruption layer that kept the legacy schema out of the new domain model. And because we wrote this code quite a while ago, we know how it aged: it ran for years with very little maintenance.</p>\n<h2>Step 1: Tracking changes with database triggers</h2>\n<p>The first problem: how do you know a product changed? The textbook answer is to publish an event from the application whenever a product is saved. The textbook answer assumes your application has one place where products get saved.</p>\n<p>Ours had many. Products were written by the shop owner panel, by the admin panel, by CSV importers, by background jobs, and by a couple of raw SQL statements scattered where you would least expect them. Finding and instrumenting every write path in a legacy codebase is exactly the kind of archaeology the Strangler Pattern is supposed to spare you. It is just risky, and the chances you will miss one are too high.</p>\n<p>So we went one level below the application, where all those write paths converge anyway: the database. A queue table and a set of triggers:</p>\n<div class=\"gatsby-highlight\" data-language=\"sql\"><pre class=\"language-sql\"><code class=\"language-sql\"><span class=\"token keyword\">CREATE</span> <span class=\"token keyword\">TABLE</span> <span class=\"token identifier\"><span class=\"token punctuation\">`</span>shop_item_updated<span class=\"token punctuation\">`</span></span> <span class=\"token punctuation\">(</span>\n  <span class=\"token identifier\"><span class=\"token punctuation\">`</span>shop_item_id<span class=\"token punctuation\">`</span></span> <span class=\"token keyword\">bigint</span><span class=\"token punctuation\">(</span><span class=\"token number\">20</span><span class=\"token punctuation\">)</span> <span class=\"token keyword\">unsigned</span> <span class=\"token operator\">NOT</span> <span class=\"token boolean\">NULL</span><span class=\"token punctuation\">,</span>\n  <span class=\"token identifier\"><span class=\"token punctuation\">`</span>updated_at<span class=\"token punctuation\">`</span></span> <span class=\"token keyword\">TIMESTAMP</span> <span class=\"token operator\">NOT</span> <span class=\"token boolean\">NULL</span><span class=\"token punctuation\">,</span>\n  <span class=\"token keyword\">PRIMARY</span> <span class=\"token keyword\">KEY</span> <span class=\"token punctuation\">(</span><span class=\"token identifier\"><span class=\"token punctuation\">`</span>shop_item_id<span class=\"token punctuation\">`</span></span><span class=\"token punctuation\">)</span>\n<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n\n<span class=\"token keyword\">CREATE</span> <span class=\"token keyword\">TRIGGER</span> after_shop_item_updates\n    <span class=\"token keyword\">AFTER</span> <span class=\"token keyword\">UPDATE</span> <span class=\"token keyword\">ON</span> shop_item\n    <span class=\"token keyword\">FOR EACH ROW</span>\n<span class=\"token keyword\">BEGIN</span>\n    <span class=\"token keyword\">INSERT</span> <span class=\"token keyword\">INTO</span> shop_item_updated <span class=\"token punctuation\">(</span>shop_item_id<span class=\"token punctuation\">,</span> updated_at<span class=\"token punctuation\">)</span>\n    <span class=\"token keyword\">VALUES</span> <span class=\"token punctuation\">(</span>OLD<span class=\"token punctuation\">.</span>id<span class=\"token punctuation\">,</span> <span class=\"token function\">NOW</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span>\n    <span class=\"token keyword\">ON</span> <span class=\"token keyword\">DUPLICATE</span> <span class=\"token keyword\">KEY</span> <span class=\"token keyword\">UPDATE</span> updated_at <span class=\"token operator\">=</span> <span class=\"token function\">NOW</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n<span class=\"token keyword\">END</span><span class=\"token punctuation\">;</span>\n\n<span class=\"token keyword\">CREATE</span> <span class=\"token keyword\">TRIGGER</span> after_shop_item_updates_insert\n    <span class=\"token keyword\">AFTER</span> <span class=\"token keyword\">INSERT</span> <span class=\"token keyword\">ON</span> shop_item\n    <span class=\"token keyword\">FOR EACH ROW</span>\n<span class=\"token keyword\">BEGIN</span>\n    <span class=\"token keyword\">INSERT</span> <span class=\"token keyword\">INTO</span> shop_item_updated <span class=\"token punctuation\">(</span>shop_item_id<span class=\"token punctuation\">,</span> updated_at<span class=\"token punctuation\">)</span>\n    <span class=\"token keyword\">VALUES</span> <span class=\"token punctuation\">(</span>NEW<span class=\"token punctuation\">.</span>id<span class=\"token punctuation\">,</span> <span class=\"token function\">NOW</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n<span class=\"token keyword\">END</span><span class=\"token punctuation\">;</span></code></pre></div>\n<p>Two design details here matter more than they look.</p>\n<p><strong>The queue stores dirty flags, not events.</strong> There is no payload, no \"what changed\", no event type. Just \"product 4711 changed at 14:32\". That has a lovely consequence: the primary key on <code class=\"language-text\">shop_item_id</code> plus <code class=\"language-text\">ON DUPLICATE KEY UPDATE</code> means the queue naturally coalesces. A product edited fifteen times between two sync runs occupies one row and gets synced once. The queue physically cannot grow beyond the number of products. You get deduplication for free, from a primary key.</p>\n<p>If you know the theory: this is a poor man's <a href=\"https://microservices.io/patterns/data/transactional-outbox.html\">transactional outbox</a> crossed with change data capture. The trigger runs in the same transaction as the write, so a change and its queue entry are atomic - you cannot lose a change the way you can with \"save, then publish to a broker\". Tools like Debezium give you the same guarantee by tailing the binlog, with ordering and payloads on top. We did not need ordering or payloads, so we did not pay for them. And the price of such tools is easy to underestimate: adding Debezium is a day of work, but from that day on you are running and monitoring Kafka Connect, upgrading it, and fixing it when a connector silently stops. With the trigger approach, there was simply nothing extra to operate.</p>\n<p>A product page is not built from one table, though. It also depends on related tables like translations, so those got triggers too - each one resolving back to the affected product IDs:</p>\n<div class=\"gatsby-highlight\" data-language=\"sql\"><pre class=\"language-sql\"><code class=\"language-sql\"><span class=\"token keyword\">CREATE</span> <span class=\"token keyword\">TRIGGER</span> after_shop_item_language_updates\n    <span class=\"token keyword\">AFTER</span> <span class=\"token keyword\">UPDATE</span> <span class=\"token keyword\">ON</span> shop_item_language\n    <span class=\"token keyword\">FOR EACH ROW</span>\n<span class=\"token keyword\">BEGIN</span>\n    <span class=\"token keyword\">INSERT</span> <span class=\"token keyword\">INTO</span> shop_item_updated <span class=\"token punctuation\">(</span>shop_item_id<span class=\"token punctuation\">,</span> updated_at<span class=\"token punctuation\">)</span>\n    <span class=\"token keyword\">VALUES</span> <span class=\"token punctuation\">(</span>OLD<span class=\"token punctuation\">.</span>shop_item_id<span class=\"token punctuation\">,</span> <span class=\"token function\">NOW</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span>\n    <span class=\"token keyword\">ON</span> <span class=\"token keyword\">DUPLICATE</span> <span class=\"token keyword\">KEY</span> <span class=\"token keyword\">UPDATE</span> updated_at <span class=\"token operator\">=</span> <span class=\"token function\">NOW</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n<span class=\"token keyword\">END</span><span class=\"token punctuation\">;</span></code></pre></div>\n<p>And the same pattern was repeated for the other synced entities like shops - one queue table per aggregate.</p>\n<p><strong>The mechanism evolved through production bugs</strong>, which I think is worth admitting. Products on this platform have child variants, and the first version of the trigger only enqueued the row that changed. Result: edit a parent product, and its variants quietly kept serving stale data. The fix was a follow-up migration that widened the trigger:</p>\n<div class=\"gatsby-highlight\" data-language=\"sql\"><pre class=\"language-sql\"><code class=\"language-sql\"><span class=\"token keyword\">CREATE</span> <span class=\"token keyword\">TRIGGER</span> <span class=\"token identifier\"><span class=\"token punctuation\">`</span>after_shop_item_updates<span class=\"token punctuation\">`</span></span>\n    <span class=\"token keyword\">AFTER</span> <span class=\"token keyword\">UPDATE</span> <span class=\"token keyword\">ON</span> <span class=\"token identifier\"><span class=\"token punctuation\">`</span>shop_item<span class=\"token punctuation\">`</span></span>\n    <span class=\"token keyword\">FOR EACH ROW</span>\n<span class=\"token keyword\">BEGIN</span>\n    <span class=\"token keyword\">INSERT</span> <span class=\"token keyword\">INTO</span> shop_item_updated\n    <span class=\"token keyword\">SELECT</span> id<span class=\"token punctuation\">,</span> <span class=\"token function\">NOW</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token keyword\">FROM</span> shop_item\n    <span class=\"token keyword\">WHERE</span> id <span class=\"token operator\">=</span> OLD<span class=\"token punctuation\">.</span>id <span class=\"token operator\">OR</span> parent_id <span class=\"token operator\">=</span> OLD<span class=\"token punctuation\">.</span>id\n    <span class=\"token keyword\">ON</span> <span class=\"token keyword\">DUPLICATE</span> <span class=\"token keyword\">KEY</span> <span class=\"token keyword\">UPDATE</span> updated_at <span class=\"token operator\">=</span> <span class=\"token function\">NOW</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n<span class=\"token keyword\">END</span></code></pre></div>\n<p>This is the trade you make with triggers: the logic of \"what does this change affect\" lives in SQL, in the database, where nobody is looking. Trigger logic does not show up in the PHP codebase, code search does not find it, and new developers do not know it exists. If you go this route, treat triggers as first-class code: keep them in versioned migration files, mention them in the developer docs, and keep them <em>dumb</em> - flag an ID, nothing more. Thankfully, we caught the issue very fast, and the fix was easy. </p>\n<h2>Step 2: Exposing the changes - the legacy half of the ACL</h2>\n<p>The new application could have just connected to the legacy database and read the queue table directly. We deliberately did not do that. Instead, the legacy application exposes an internal HTTP API, and this is where the anti-corruption layer starts - on the <em>legacy</em> side. </p>\n<p>The endpoint joins the queue table against the product data and returns a batch, oldest changes first:</p>\n<div class=\"gatsby-highlight\" data-language=\"php\"><pre class=\"language-php\"><code class=\"language-php\"><span class=\"token comment\">// Legacy (Zend) - the internal sync endpoint, trimmed</span>\n<span class=\"token keyword\">public</span> <span class=\"token keyword\">function</span> <span class=\"token function-definition function\">listproductsAction</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span>\n<span class=\"token punctuation\">{</span>\n    <span class=\"token variable\">$this</span><span class=\"token operator\">-></span><span class=\"token function\">checkIp</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n\n    <span class=\"token variable\">$shopItemModel</span> <span class=\"token operator\">=</span> <span class=\"token keyword\">new</span> <span class=\"token class-name\">Default_Model_ShopItem</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n    <span class=\"token variable\">$shopItems</span> <span class=\"token operator\">=</span> <span class=\"token variable\">$shopItemModel</span><span class=\"token operator\">-></span><span class=\"token function\">getForSyncWithJoins</span><span class=\"token punctuation\">(</span><span class=\"token variable\">$this</span><span class=\"token operator\">-></span><span class=\"token function\">getParam</span><span class=\"token punctuation\">(</span><span class=\"token string single-quoted-string\">'limit'</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n\n    <span class=\"token variable\">$productMapper</span> <span class=\"token operator\">=</span> <span class=\"token keyword\">new</span> <span class=\"token class-name\">ProductMapper</span><span class=\"token punctuation\">(</span><span class=\"token variable\">$this</span><span class=\"token operator\">-></span><span class=\"token property\">view</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n    <span class=\"token variable\">$mapped</span> <span class=\"token operator\">=</span> <span class=\"token punctuation\">[</span><span class=\"token punctuation\">]</span><span class=\"token punctuation\">;</span>\n\n    <span class=\"token comment\">// $images, $children and $categoryMapper are prepared</span>\n    <span class=\"token comment\">// for the whole batch - trimmed for brevity</span>\n    <span class=\"token keyword\">foreach</span> <span class=\"token punctuation\">(</span><span class=\"token variable\">$shopItems</span> <span class=\"token keyword\">as</span> <span class=\"token variable\">$row</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n        <span class=\"token variable\">$shopItem</span> <span class=\"token operator\">=</span> <span class=\"token class-name static-context\">ShopItemFactory</span><span class=\"token operator\">::</span><span class=\"token function\">create</span><span class=\"token punctuation\">(</span><span class=\"token variable\">$row</span><span class=\"token punctuation\">,</span> <span class=\"token variable\">$images</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n        <span class=\"token variable\">$product</span> <span class=\"token operator\">=</span> <span class=\"token variable\">$productMapper</span><span class=\"token operator\">-></span><span class=\"token function\">mapToProduct</span><span class=\"token punctuation\">(</span><span class=\"token variable\">$row</span><span class=\"token punctuation\">,</span> <span class=\"token variable\">$shopItem</span><span class=\"token punctuation\">,</span> <span class=\"token variable\">$children</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n        <span class=\"token variable\">$product</span><span class=\"token punctuation\">[</span><span class=\"token string single-quoted-string\">'category'</span><span class=\"token punctuation\">]</span> <span class=\"token operator\">=</span> <span class=\"token variable\">$categoryMapper</span><span class=\"token operator\">-></span><span class=\"token function\">mapToTypeAndAssortment</span><span class=\"token punctuation\">(</span>\n            <span class=\"token variable\">$shopItem</span><span class=\"token operator\">-></span><span class=\"token function\">getPatternCategoryId</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span>\n        <span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n\n        <span class=\"token variable\">$mapped</span><span class=\"token punctuation\">[</span><span class=\"token punctuation\">]</span> <span class=\"token operator\">=</span> <span class=\"token variable\">$product</span><span class=\"token punctuation\">;</span>\n    <span class=\"token punctuation\">}</span>\n\n    <span class=\"token variable\">$this</span><span class=\"token operator\">-></span><span class=\"token function\">getResponse</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span>\n        <span class=\"token operator\">-></span><span class=\"token function\">setBody</span><span class=\"token punctuation\">(</span><span class=\"token function\">json_encode</span><span class=\"token punctuation\">(</span><span class=\"token variable\">$mapped</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span>\n        <span class=\"token operator\">-></span><span class=\"token function\">setHttpResponseCode</span><span class=\"token punctuation\">(</span><span class=\"token number\">200</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span></code></pre></div>\n<p>The interesting part is what <code class=\"language-text\">ProductMapper</code> does. It does <strong>not</strong> dump the legacy row. It builds a deliberate contract - clean names, resolved relations, computed flags:</p>\n<div class=\"gatsby-highlight\" data-language=\"php\"><pre class=\"language-php\"><code class=\"language-php\"><span class=\"token comment\">// Legacy - mapping internal mess into a published contract</span>\n<span class=\"token keyword\">public</span> <span class=\"token keyword\">function</span> <span class=\"token function-definition function\">mapToProduct</span><span class=\"token punctuation\">(</span><span class=\"token keyword type-hint\">array</span> <span class=\"token variable\">$row</span><span class=\"token punctuation\">,</span> <span class=\"token class-name type-declaration\">Default_Entity_ShopItem</span> <span class=\"token variable\">$shopItem</span><span class=\"token punctuation\">,</span> <span class=\"token operator\">...</span><span class=\"token punctuation\">)</span>\n<span class=\"token punctuation\">{</span>\n    <span class=\"token keyword\">return</span> <span class=\"token punctuation\">[</span>\n        <span class=\"token string single-quoted-string\">'id'</span> <span class=\"token operator\">=></span> <span class=\"token variable\">$shopItem</span><span class=\"token operator\">-></span><span class=\"token function\">getId</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\n        <span class=\"token string single-quoted-string\">'name'</span> <span class=\"token operator\">=></span> <span class=\"token variable\">$shopItem</span><span class=\"token operator\">-></span><span class=\"token function\">getName</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\n        <span class=\"token string single-quoted-string\">'price'</span> <span class=\"token operator\">=></span> <span class=\"token variable\">$shopItem</span><span class=\"token operator\">-></span><span class=\"token function\">getPrice</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\n        <span class=\"token string single-quoted-string\">'description'</span> <span class=\"token operator\">=></span> <span class=\"token function\">html_entity_decode</span><span class=\"token punctuation\">(</span><span class=\"token variable\">$shopItem</span><span class=\"token operator\">-></span><span class=\"token function\">getDescription</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\n        <span class=\"token string single-quoted-string\">'updated_at'</span> <span class=\"token operator\">=></span> <span class=\"token variable\">$row</span><span class=\"token punctuation\">[</span><span class=\"token string single-quoted-string\">'updated_at'</span><span class=\"token punctuation\">]</span><span class=\"token punctuation\">,</span>\n        <span class=\"token string single-quoted-string\">'shop'</span> <span class=\"token operator\">=></span> <span class=\"token punctuation\">[</span>\n            <span class=\"token string single-quoted-string\">'subdomain'</span> <span class=\"token operator\">=></span> <span class=\"token variable\">$shopItem</span><span class=\"token operator\">-></span><span class=\"token function\">getSubdomain</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\n            <span class=\"token string single-quoted-string\">'externalUri'</span> <span class=\"token operator\">=></span> <span class=\"token variable\">$shopItem</span><span class=\"token operator\">-></span><span class=\"token function\">getExternalDomain</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\n            <span class=\"token string single-quoted-string\">'id'</span> <span class=\"token operator\">=></span> <span class=\"token variable\">$shopItem</span><span class=\"token operator\">-></span><span class=\"token function\">getShopId</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\n        <span class=\"token punctuation\">]</span><span class=\"token punctuation\">,</span>\n        <span class=\"token comment\">// three legacy flags collapse into one meaningful boolean</span>\n        <span class=\"token string single-quoted-string\">'deleted'</span> <span class=\"token operator\">=></span> <span class=\"token variable\">$row</span><span class=\"token punctuation\">[</span><span class=\"token string single-quoted-string\">'is_deleted'</span><span class=\"token punctuation\">]</span>\n            <span class=\"token operator\">||</span> <span class=\"token variable\">$row</span><span class=\"token punctuation\">[</span><span class=\"token string single-quoted-string\">'parent_is_deleted'</span><span class=\"token punctuation\">]</span>\n            <span class=\"token operator\">||</span> <span class=\"token variable\">$row</span><span class=\"token punctuation\">[</span><span class=\"token string single-quoted-string\">'pattern_is_deleted'</span><span class=\"token punctuation\">]</span><span class=\"token punctuation\">,</span>\n        <span class=\"token string single-quoted-string\">'enabled'</span> <span class=\"token operator\">=></span> <span class=\"token operator\">!</span><span class=\"token variable\">$shopItem</span><span class=\"token operator\">-></span><span class=\"token function\">isDisabled</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token operator\">&amp;&amp;</span> <span class=\"token operator\">!</span><span class=\"token variable\">$row</span><span class=\"token punctuation\">[</span><span class=\"token string single-quoted-string\">'pattern_is_hidden'</span><span class=\"token punctuation\">]</span><span class=\"token punctuation\">,</span>\n        <span class=\"token comment\">// ...</span>\n    <span class=\"token punctuation\">]</span><span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span></code></pre></div>\n<p>Look at that <code class=\"language-text\">deleted</code> line. In the legacy schema, \"is this product gone\" is a question you can only answer by checking three flags across three tables - the product, its parent, and its pattern. The mapper answers the question once, at the boundary, and the new system never learns the question existed. That is anti-corruption in one line of code.</p>\n<p>In DDD terms this endpoint is an <a href=\"https://ddd-practitioners.com/home/glossary/bounded-context/bounded-context-relationship/open-host-service/\">open host service</a> speaking a published language: the legacy context translates its internal chaos into an agreed contract before anything crosses the boundary. It also means the legacy team can refactor their tables without breaking the new app, as long as the JSON stays the same. We got to use that freedom more than once.</p>\n<p>One comment worth mentioning here: the three different delete flags might make sense in some cases, but not in ours. We were rebuilding a product catalogue, and for a catalogue it does not matter why a product got deleted. We only care whether it is deleted. If you know you will need those distinctions later, you might want to expose the flags separately and implement that part of the ACL in the new system.</p>\n<h2>Step 3: Pulling the changes - the cron job</h2>\n<p>On the Symfony side, a single console command runs from cron:</p>\n<div class=\"gatsby-highlight\" data-language=\"yaml\"><pre class=\"language-yaml\"><code class=\"language-yaml\"><span class=\"token comment\"># ansible crontab role</span>\n<span class=\"token punctuation\">-</span> <span class=\"token key atrule\">name</span><span class=\"token punctuation\">:</span> sync\n  <span class=\"token key atrule\">minute</span><span class=\"token punctuation\">:</span> <span class=\"token string\">\"*/3\"</span>\n  <span class=\"token key atrule\">job</span><span class=\"token punctuation\">:</span> <span class=\"token string\">\"php bin/console -eprod app:sync -v >> cron.log 2>&amp;1\"</span></code></pre></div>\n<p>Every three minutes. That number was not scientific - it was \"fast enough that no shop owner ever complained, slow enough that the batches stay small\". We never had to change it.</p>\n<p>The command itself is mostly plumbing: a lock so runs cannot overlap, and a list of sync providers:</p>\n<div class=\"gatsby-highlight\" data-language=\"php\"><pre class=\"language-php\"><code class=\"language-php\"><span class=\"token keyword\">class</span> <span class=\"token class-name-definition class-name\">SyncCommand</span> <span class=\"token keyword\">extends</span> <span class=\"token class-name\">Command</span>\n<span class=\"token punctuation\">{</span>\n    <span class=\"token keyword\">use</span> <span class=\"token package\">LockableTrait</span><span class=\"token punctuation\">;</span>\n\n    <span class=\"token keyword\">const</span> <span class=\"token constant\">TYPES</span> <span class=\"token operator\">=</span> <span class=\"token punctuation\">[</span><span class=\"token string single-quoted-string\">'shop'</span><span class=\"token punctuation\">,</span> <span class=\"token string single-quoted-string\">'product'</span> <span class=\"token comment\">/*,  ... and more */</span><span class=\"token punctuation\">]</span><span class=\"token punctuation\">;</span>\n\n    <span class=\"token keyword\">protected</span> <span class=\"token keyword\">function</span> <span class=\"token function-definition function\">execute</span><span class=\"token punctuation\">(</span><span class=\"token class-name type-declaration\">InputInterface</span> <span class=\"token variable\">$input</span><span class=\"token punctuation\">,</span> <span class=\"token class-name type-declaration\">OutputInterface</span> <span class=\"token variable\">$output</span><span class=\"token punctuation\">)</span>\n    <span class=\"token punctuation\">{</span>\n        <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span><span class=\"token operator\">!</span><span class=\"token variable\">$this</span><span class=\"token operator\">-></span><span class=\"token function\">lock</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n            <span class=\"token variable\">$output</span><span class=\"token operator\">-></span><span class=\"token function\">writeln</span><span class=\"token punctuation\">(</span><span class=\"token string single-quoted-string\">'The command is already running in another process.'</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n            <span class=\"token keyword\">return</span><span class=\"token punctuation\">;</span>\n        <span class=\"token punctuation\">}</span>\n\n        <span class=\"token keyword\">try</span> <span class=\"token punctuation\">{</span>\n            <span class=\"token variable\">$this</span><span class=\"token operator\">-></span><span class=\"token property\">syncManager</span><span class=\"token operator\">-></span><span class=\"token function\">syncAll</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n        <span class=\"token punctuation\">}</span> <span class=\"token keyword\">catch</span> <span class=\"token punctuation\">(</span><span class=\"token class-name class-name-fully-qualified\"><span class=\"token punctuation\">\\</span>Exception</span> <span class=\"token variable\">$e</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n            <span class=\"token variable\">$this</span><span class=\"token operator\">-></span><span class=\"token function\">release</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n            <span class=\"token keyword\">throw</span> <span class=\"token variable\">$e</span><span class=\"token punctuation\">;</span>\n        <span class=\"token punctuation\">}</span>\n        <span class=\"token comment\">// ...</span>\n        <span class=\"token variable\">$this</span><span class=\"token operator\">-></span><span class=\"token function\">release</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n    <span class=\"token punctuation\">}</span>\n<span class=\"token punctuation\">}</span></code></pre></div>\n<p>The <code class=\"language-text\">LockableTrait</code> is doing quiet but essential work here. Batches sometimes take longer than three minutes - a big CSV import on the legacy side can enqueue thousands of products at once. Without the lock, cron would happily start a second run on top of the first, and you would sync the same products twice, concurrently, into the same tables. One trait, one line, whole class of bugs gone.</p>\n<p>Synchronizers register through a Symfony tag with priorities, because order matters - you cannot sync a product into a shop that does not exist yet:</p>\n<div class=\"gatsby-highlight\" data-language=\"yaml\"><pre class=\"language-yaml\"><code class=\"language-yaml\"><span class=\"token key atrule\">App\\Infrastructure\\Synchronizer\\ShopSynchronizer</span><span class=\"token punctuation\">:</span>\n    <span class=\"token key atrule\">tags</span><span class=\"token punctuation\">:</span> <span class=\"token punctuation\">[</span><span class=\"token punctuation\">{</span> <span class=\"token key atrule\">name</span><span class=\"token punctuation\">:</span> synchronizer<span class=\"token punctuation\">,</span> <span class=\"token key atrule\">priority</span><span class=\"token punctuation\">:</span> <span class=\"token number\">90</span> <span class=\"token punctuation\">}</span><span class=\"token punctuation\">]</span>\n<span class=\"token key atrule\">App\\Infrastructure\\Synchronizer\\ProductSynchronizer</span><span class=\"token punctuation\">:</span>\n    <span class=\"token key atrule\">tags</span><span class=\"token punctuation\">:</span> <span class=\"token punctuation\">[</span><span class=\"token punctuation\">{</span> <span class=\"token key atrule\">name</span><span class=\"token punctuation\">:</span> synchronizer<span class=\"token punctuation\">,</span> <span class=\"token key atrule\">priority</span><span class=\"token punctuation\">:</span> <span class=\"token number\">80</span> <span class=\"token punctuation\">}</span><span class=\"token punctuation\">]</span></code></pre></div>\n<p>Shops before products. Dependency ordering as configuration - simple and visible.</p>\n<p>The product synchronizer is where the batch is processed. Trimmed to its essence:</p>\n<div class=\"gatsby-highlight\" data-language=\"php\"><pre class=\"language-php\"><code class=\"language-php\"><span class=\"token keyword\">class</span> <span class=\"token class-name-definition class-name\">ProductSynchronizer</span> <span class=\"token keyword\">implements</span> <span class=\"token class-name\">Synchronizer</span>\n<span class=\"token punctuation\">{</span>\n    <span class=\"token keyword\">const</span> <span class=\"token constant\">LIMIT</span> <span class=\"token operator\">=</span> <span class=\"token number\">200</span><span class=\"token punctuation\">;</span>\n\n    <span class=\"token keyword\">public</span> <span class=\"token keyword\">function</span> <span class=\"token function-definition function\">sync</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span>\n    <span class=\"token punctuation\">{</span>\n        <span class=\"token variable\">$timestamp</span> <span class=\"token operator\">=</span> <span class=\"token punctuation\">(</span><span class=\"token keyword\">new</span> <span class=\"token class-name class-name-fully-qualified\"><span class=\"token punctuation\">\\</span>DateTime</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span><span class=\"token operator\">-></span><span class=\"token function\">getTimestamp</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n        <span class=\"token variable\">$productsData</span> <span class=\"token operator\">=</span> <span class=\"token variable\">$this</span><span class=\"token operator\">-></span><span class=\"token property\">productApi</span><span class=\"token operator\">-></span><span class=\"token function\">findAll</span><span class=\"token punctuation\">(</span><span class=\"token keyword static-context\">self</span><span class=\"token operator\">::</span><span class=\"token constant\">LIMIT</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n        <span class=\"token variable\">$synchronized</span> <span class=\"token operator\">=</span> <span class=\"token punctuation\">[</span><span class=\"token punctuation\">]</span><span class=\"token punctuation\">;</span>\n\n        <span class=\"token keyword\">foreach</span> <span class=\"token punctuation\">(</span><span class=\"token variable\">$productsData</span> <span class=\"token keyword\">as</span> <span class=\"token variable\">$product</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n            <span class=\"token keyword\">try</span> <span class=\"token punctuation\">{</span>\n                <span class=\"token variable\">$this</span><span class=\"token operator\">-></span><span class=\"token function\">synchronizeProduct</span><span class=\"token punctuation\">(</span><span class=\"token variable\">$product</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n            <span class=\"token punctuation\">}</span> <span class=\"token keyword\">catch</span> <span class=\"token punctuation\">(</span><span class=\"token class-name class-name-fully-qualified\"><span class=\"token punctuation\">\\</span>Exception</span> <span class=\"token variable\">$e</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n                <span class=\"token variable\">$this</span><span class=\"token operator\">-></span><span class=\"token function\">logError</span><span class=\"token punctuation\">(</span><span class=\"token keyword static-context\">self</span><span class=\"token operator\">::</span><span class=\"token constant\">LOG_MESSAGE</span><span class=\"token punctuation\">,</span> <span class=\"token punctuation\">[</span><span class=\"token string single-quoted-string\">'error'</span> <span class=\"token operator\">=></span> <span class=\"token variable\">$e</span><span class=\"token operator\">-></span><span class=\"token function\">getMessage</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">]</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n\n                <span class=\"token keyword\">continue</span><span class=\"token punctuation\">;</span>   <span class=\"token comment\">// one broken product must not stop the batch</span>\n            <span class=\"token punctuation\">}</span>\n\n            <span class=\"token variable\">$synchronized</span><span class=\"token punctuation\">[</span><span class=\"token punctuation\">]</span> <span class=\"token operator\">=</span> <span class=\"token variable\">$this</span><span class=\"token operator\">-></span><span class=\"token property\">productTranslator</span><span class=\"token operator\">-></span><span class=\"token function\">getId</span><span class=\"token punctuation\">(</span><span class=\"token variable\">$product</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n        <span class=\"token punctuation\">}</span>\n\n        <span class=\"token variable\">$this</span><span class=\"token operator\">-></span><span class=\"token property\">em</span><span class=\"token operator\">-></span><span class=\"token function\">flush</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n        <span class=\"token variable\">$this</span><span class=\"token operator\">-></span><span class=\"token property\">productApi</span><span class=\"token operator\">-></span><span class=\"token function\">markAsSynchronized</span><span class=\"token punctuation\">(</span><span class=\"token variable\">$synchronized</span><span class=\"token punctuation\">,</span> <span class=\"token variable\">$timestamp</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n    <span class=\"token punctuation\">}</span>\n<span class=\"token punctuation\">}</span></code></pre></div>\n<p>Three things to notice:</p>\n<ul>\n<li><strong>A batch limit.</strong> 200 products per run. A giant import does not blow up memory or block the cron slot for an hour; it just drains over several runs, oldest first.</li>\n<li><strong>Per-item error isolation.</strong> One product with broken data logs the error and is skipped. The other 199 sync fine. Without this, a single malformed row would wedge the entire pipeline - and in a legacy system, malformed rows are a certainty, not a risk.</li>\n<li><strong>Only successes are confirmed.</strong> The failed product's ID never makes it into <code class=\"language-text\">$synchronized</code>, so its queue row survives and it will be retried on the next run.</li>\n</ul>\n<h2>Step 4: Confirming - and why the timestamp matters</h2>\n<p>The confirmation call is the most subtle part of the whole design. The new side reports back which products it managed to process, along with a timestamp:</p>\n<div class=\"gatsby-highlight\" data-language=\"php\"><pre class=\"language-php\"><code class=\"language-php\"><span class=\"token keyword\">public</span> <span class=\"token keyword\">function</span> <span class=\"token function-definition function\">markAsSynchronized</span><span class=\"token punctuation\">(</span><span class=\"token keyword type-hint\">array</span> <span class=\"token variable\">$ids</span><span class=\"token punctuation\">,</span> <span class=\"token keyword type-hint\">int</span> <span class=\"token variable\">$timestamp</span><span class=\"token punctuation\">)</span>\n<span class=\"token punctuation\">{</span>\n    <span class=\"token variable\">$this</span><span class=\"token operator\">-></span><span class=\"token property\">client</span><span class=\"token operator\">-></span><span class=\"token function\">request</span><span class=\"token punctuation\">(</span><span class=\"token string single-quoted-string\">'POST'</span><span class=\"token punctuation\">,</span> <span class=\"token string single-quoted-string\">'synchronizedProducts'</span><span class=\"token punctuation\">,</span> <span class=\"token punctuation\">[</span>\n        <span class=\"token string single-quoted-string\">'json'</span> <span class=\"token operator\">=></span> <span class=\"token punctuation\">[</span><span class=\"token string single-quoted-string\">'ids'</span> <span class=\"token operator\">=></span> <span class=\"token variable\">$ids</span><span class=\"token punctuation\">,</span> <span class=\"token string single-quoted-string\">'timestamp'</span> <span class=\"token operator\">=></span> <span class=\"token variable\">$timestamp</span><span class=\"token punctuation\">]</span><span class=\"token punctuation\">,</span>\n    <span class=\"token punctuation\">]</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span></code></pre></div>\n<p>And the legacy side deletes the queue rows - but only if they have not been touched since:</p>\n<div class=\"gatsby-highlight\" data-language=\"sql\"><pre class=\"language-sql\"><code class=\"language-sql\"><span class=\"token keyword\">DELETE</span> <span class=\"token keyword\">FROM</span> shop_item_updated\n<span class=\"token keyword\">WHERE</span> shop_item_id <span class=\"token operator\">IN</span> <span class=\"token punctuation\">(</span>:ids<span class=\"token punctuation\">)</span>\n  <span class=\"token operator\">AND</span> UNIX_TIMESTAMP<span class=\"token punctuation\">(</span>updated_at<span class=\"token punctuation\">)</span> <span class=\"token operator\">&lt;=</span> :<span class=\"token keyword\">timestamp</span></code></pre></div>\n<p>That <code class=\"language-text\">updated_at &lt;= timestamp</code> condition is what makes the mechanism safe. Consider the race: the sync fetches product 4711, and <em>while the batch is being processed</em>, a shop owner edits that same product. The trigger bumps <code class=\"language-text\">updated_at</code> in the queue. When the confirmation arrives, the timestamp check sees a newer <code class=\"language-text\">updated_at</code> and refuses to delete the row - so the product is synced again next run, picking up the fresh edit. A naive <code class=\"language-text\">DELETE WHERE id IN (...)</code> would have silently lost that update.</p>\n<p>This gives you <strong>at-least-once delivery</strong>: a product may be synced twice, but never zero times. At-least-once only works if processing is idempotent, and here it is - the new side upserts by the legacy product number through mapping tables, so syncing the same product twice just overwrites identical data.</p>\n<p>The pattern at work here has a name: <em>at-least-once delivery plus idempotent consumer</em>. It is the boring, battle-tested foundation under most reliable messaging systems, and you can implement it with a <code class=\"language-text\">TIMESTAMP</code> column and a <code class=\"language-text\">DELETE ... WHERE</code>.</p>\n<h2>Step 5: The new side's ACL - translator, event, listener</h2>\n<p>The data that arrives is clean-ish JSON, but it is still the legacy contract - legacy IDs, legacy category names, legacy quirks like <code class=\"language-text\">0000-00-00 00:00:00</code> dates. None of that is allowed anywhere near the new domain model. The new side runs it through three stages.</p>\n<p><strong>Stage one: the translator.</strong> It absorbs the quirks and maps legacy identifiers onto the new world:</p>\n<div class=\"gatsby-highlight\" data-language=\"php\"><pre class=\"language-php\"><code class=\"language-php\"><span class=\"token keyword\">class</span> <span class=\"token class-name-definition class-name\">LegacyProductTranslator</span> <span class=\"token keyword\">implements</span> <span class=\"token class-name\">ProductTranslator</span>\n<span class=\"token punctuation\">{</span>\n    <span class=\"token keyword\">public</span> <span class=\"token keyword\">function</span> <span class=\"token function-definition function\">createFromArray</span><span class=\"token punctuation\">(</span><span class=\"token keyword type-hint\">array</span> <span class=\"token variable\">$data</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">:</span> <span class=\"token keyword return-type\">array</span>\n    <span class=\"token punctuation\">{</span>\n        <span class=\"token comment\">// legacy pattern_id -> SKU of the base item in the NEW catalog</span>\n        <span class=\"token variable\">$baseItemMap</span> <span class=\"token operator\">=</span> <span class=\"token variable\">$this</span><span class=\"token operator\">-></span><span class=\"token property\">patternMapRepository</span>\n            <span class=\"token operator\">-></span><span class=\"token function\">findOneByPatternId</span><span class=\"token punctuation\">(</span><span class=\"token variable\">$data</span><span class=\"token punctuation\">[</span><span class=\"token string single-quoted-string\">'pattern_id'</span><span class=\"token punctuation\">]</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n\n        <span class=\"token comment\">// has this legacy product been synced before?</span>\n        <span class=\"token variable\">$productMap</span> <span class=\"token operator\">=</span> <span class=\"token variable\">$this</span><span class=\"token operator\">-></span><span class=\"token property\">productVersionMapRepository</span>\n            <span class=\"token operator\">-></span><span class=\"token function\">findOneByLegacyId</span><span class=\"token punctuation\">(</span><span class=\"token variable\">$data</span><span class=\"token punctuation\">[</span><span class=\"token string single-quoted-string\">'id'</span><span class=\"token punctuation\">]</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n\n        <span class=\"token comment\">// a classic MySQL legacy quirk, absorbed here and nowhere else</span>\n        <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span><span class=\"token string single-quoted-string\">'0000-00-00 00:00:00'</span> <span class=\"token operator\">==</span> <span class=\"token variable\">$data</span><span class=\"token punctuation\">[</span><span class=\"token string single-quoted-string\">'created_at'</span><span class=\"token punctuation\">]</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n            <span class=\"token variable\">$data</span><span class=\"token punctuation\">[</span><span class=\"token string single-quoted-string\">'created_at'</span><span class=\"token punctuation\">]</span> <span class=\"token operator\">=</span> <span class=\"token string single-quoted-string\">'1970-01-01 00:00:00'</span><span class=\"token punctuation\">;</span>\n        <span class=\"token punctuation\">}</span>\n\n        <span class=\"token keyword\">return</span> <span class=\"token punctuation\">[</span>\n            <span class=\"token string single-quoted-string\">'number'</span> <span class=\"token operator\">=></span> <span class=\"token variable\">$data</span><span class=\"token punctuation\">[</span><span class=\"token string single-quoted-string\">'id'</span><span class=\"token punctuation\">]</span><span class=\"token punctuation\">,</span>\n            <span class=\"token string single-quoted-string\">'base_item_sku'</span> <span class=\"token operator\">=></span> <span class=\"token variable\">$baseItemMap</span> <span class=\"token operator\">?</span> <span class=\"token variable\">$baseItemMap</span><span class=\"token operator\">-></span><span class=\"token function\">getBaseItemSku</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">:</span> <span class=\"token class-name return-type\">null</span><span class=\"token punctuation\">,</span>\n            <span class=\"token string single-quoted-string\">'current_product_sku'</span> <span class=\"token operator\">=></span> <span class=\"token variable\">$productMap</span> <span class=\"token operator\">?</span> <span class=\"token variable\">$productMap</span><span class=\"token operator\">-></span><span class=\"token function\">getProductSku</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">:</span> <span class=\"token class-name return-type\">null</span><span class=\"token punctuation\">,</span>\n            <span class=\"token string single-quoted-string\">'name'</span> <span class=\"token operator\">=></span> <span class=\"token variable\">$data</span><span class=\"token punctuation\">[</span><span class=\"token string single-quoted-string\">'name'</span><span class=\"token punctuation\">]</span><span class=\"token punctuation\">,</span>\n            <span class=\"token string single-quoted-string\">'created_at'</span> <span class=\"token operator\">=></span> <span class=\"token keyword\">new</span> <span class=\"token class-name class-name-fully-qualified\"><span class=\"token punctuation\">\\</span>DateTimeImmutable</span><span class=\"token punctuation\">(</span><span class=\"token variable\">$data</span><span class=\"token punctuation\">[</span><span class=\"token string single-quoted-string\">'created_at'</span><span class=\"token punctuation\">]</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\n            <span class=\"token string single-quoted-string\">'enabled'</span> <span class=\"token operator\">=></span> <span class=\"token function\">filter_var</span><span class=\"token punctuation\">(</span><span class=\"token variable\">$data</span><span class=\"token punctuation\">[</span><span class=\"token string single-quoted-string\">'enabled'</span><span class=\"token punctuation\">]</span><span class=\"token punctuation\">,</span> <span class=\"token constant\">FILTER_VALIDATE_BOOLEAN</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\n            <span class=\"token string single-quoted-string\">'deleted'</span> <span class=\"token operator\">=></span> <span class=\"token function\">filter_var</span><span class=\"token punctuation\">(</span><span class=\"token variable\">$data</span><span class=\"token punctuation\">[</span><span class=\"token string single-quoted-string\">'deleted'</span><span class=\"token punctuation\">]</span><span class=\"token punctuation\">,</span> <span class=\"token constant\">FILTER_VALIDATE_BOOLEAN</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\n            <span class=\"token string single-quoted-string\">'prices'</span> <span class=\"token operator\">=></span> <span class=\"token punctuation\">[</span>\n                <span class=\"token variable\">$shop</span><span class=\"token operator\">-></span><span class=\"token function\">getDefaultCurrency</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token operator\">-></span><span class=\"token function\">getIsoCode</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token operator\">=></span> <span class=\"token variable\">$data</span><span class=\"token punctuation\">[</span><span class=\"token string single-quoted-string\">'price'</span><span class=\"token punctuation\">]</span><span class=\"token punctuation\">,</span>\n            <span class=\"token punctuation\">]</span><span class=\"token punctuation\">,</span>\n            <span class=\"token comment\">// ...</span>\n        <span class=\"token punctuation\">]</span><span class=\"token punctuation\">;</span>\n    <span class=\"token punctuation\">}</span>\n<span class=\"token punctuation\">}</span></code></pre></div>\n<p>Those two repository lookups are the heart of the ACL. The old and the new system do not share identifiers - the legacy world thinks in numeric <code class=\"language-text\">pattern_id</code>s, the new catalog thinks in SKUs. Dedicated <strong>mapping tables</strong> (<code class=\"language-text\">BaseItemVersionToPatternMap</code>, <code class=\"language-text\">ProductVersionToLegacyMap</code>) are the only place where both ID schemes appear together. The domain model itself carries no <code class=\"language-text\">legacyId</code> column. When legacy dies, you drop the mapping tables and the domain does not notice.</p>\n<p><strong>Stage two: a validated event.</strong> The translated payload becomes an explicit event with the contract enforced in the constructor:</p>\n<div class=\"gatsby-highlight\" data-language=\"php\"><pre class=\"language-php\"><code class=\"language-php\"><span class=\"token keyword\">class</span> <span class=\"token class-name-definition class-name\">ProductDataChangedEvent</span>\n<span class=\"token punctuation\">{</span>\n    <span class=\"token keyword\">public</span> <span class=\"token keyword\">static</span> <span class=\"token keyword\">function</span> <span class=\"token function-definition function\">create</span><span class=\"token punctuation\">(</span><span class=\"token keyword type-hint\">array</span> <span class=\"token variable\">$payload</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">:</span> <span class=\"token keyword return-type\">self</span>\n    <span class=\"token punctuation\">{</span>\n        <span class=\"token variable\">$self</span> <span class=\"token operator\">=</span> <span class=\"token keyword\">new</span> <span class=\"token keyword\">self</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n        <span class=\"token variable\">$self</span><span class=\"token operator\">-></span><span class=\"token function\">validate</span><span class=\"token punctuation\">(</span><span class=\"token variable\">$payload</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>   <span class=\"token comment\">// asserts required fields exist</span>\n        <span class=\"token variable\">$self</span><span class=\"token operator\">-></span><span class=\"token property\">payload</span> <span class=\"token operator\">=</span> <span class=\"token variable\">$payload</span><span class=\"token punctuation\">;</span>\n\n        <span class=\"token keyword\">return</span> <span class=\"token variable\">$self</span><span class=\"token punctuation\">;</span>\n    <span class=\"token punctuation\">}</span>\n\n    <span class=\"token keyword\">public</span> <span class=\"token keyword\">function</span> <span class=\"token function-definition function\">getName</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">:</span> <span class=\"token keyword return-type\">string</span>\n    <span class=\"token punctuation\">{</span>\n        <span class=\"token keyword\">return</span> <span class=\"token variable\">$this</span><span class=\"token operator\">-></span><span class=\"token property\">payload</span><span class=\"token punctuation\">[</span><span class=\"token string single-quoted-string\">'name'</span><span class=\"token punctuation\">]</span><span class=\"token punctuation\">;</span>\n    <span class=\"token punctuation\">}</span>\n    <span class=\"token comment\">// ... typed getters for every field</span>\n<span class=\"token punctuation\">}</span></code></pre></div>\n<p><strong>Stage three: a domain listener</strong> that speaks only the new language. This is the first place in the whole pipeline where domain objects are built, and notice what it imports - repositories, value objects, factories from the new bounded context. Not one legacy concept:</p>\n<div class=\"gatsby-highlight\" data-language=\"php\"><pre class=\"language-php\"><code class=\"language-php\"><span class=\"token keyword\">class</span> <span class=\"token class-name-definition class-name\">UpdateProductEventListener</span>\n<span class=\"token punctuation\">{</span>\n    <span class=\"token keyword\">public</span> <span class=\"token keyword\">function</span> <span class=\"token function-definition function\">__invoke</span><span class=\"token punctuation\">(</span><span class=\"token class-name type-declaration\">ProductDataChangedEvent</span> <span class=\"token variable\">$event</span><span class=\"token punctuation\">)</span>\n    <span class=\"token punctuation\">{</span>\n        <span class=\"token variable\">$baseItem</span> <span class=\"token operator\">=</span> <span class=\"token variable\">$this</span><span class=\"token operator\">-></span><span class=\"token property\">baseItemRepository</span><span class=\"token operator\">-></span><span class=\"token function\">findOneBySku</span><span class=\"token punctuation\">(</span><span class=\"token variable\">$event</span><span class=\"token operator\">-></span><span class=\"token function\">getBaseItemSku</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n        <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span><span class=\"token operator\">!</span><span class=\"token variable\">$baseItem</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n            <span class=\"token keyword\">return</span><span class=\"token punctuation\">;</span>   <span class=\"token comment\">// base item not in the new catalog -> not our concern yet</span>\n        <span class=\"token punctuation\">}</span>\n\n        <span class=\"token variable\">$shop</span> <span class=\"token operator\">=</span> <span class=\"token variable\">$this</span><span class=\"token operator\">-></span><span class=\"token property\">shopRepository</span><span class=\"token operator\">-></span><span class=\"token function\">findOneByIdentifier</span><span class=\"token punctuation\">(</span><span class=\"token variable\">$event</span><span class=\"token operator\">-></span><span class=\"token function\">getShopIdentifier</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n\n        <span class=\"token variable\">$sku</span> <span class=\"token operator\">=</span> <span class=\"token variable\">$baseItem</span><span class=\"token operator\">-></span><span class=\"token function\">getSku</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token operator\">.</span> <span class=\"token string single-quoted-string\">'_'</span> <span class=\"token operator\">.</span> <span class=\"token variable\">$this</span><span class=\"token operator\">-></span><span class=\"token property\">skuGenerator</span><span class=\"token operator\">-></span><span class=\"token function\">getSKU</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token operator\">.</span> <span class=\"token string single-quoted-string\">'_'</span> <span class=\"token operator\">.</span> <span class=\"token variable\">$event</span><span class=\"token operator\">-></span><span class=\"token function\">getNumber</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n\n        <span class=\"token variable\">$commission</span> <span class=\"token operator\">=</span> <span class=\"token class-name static-context\">Commission</span><span class=\"token operator\">::</span><span class=\"token constant\">PERCENTAGE</span> <span class=\"token operator\">===</span> <span class=\"token variable\">$event</span><span class=\"token operator\">-></span><span class=\"token function\">getCommissionType</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span>\n            <span class=\"token operator\">?</span> <span class=\"token class-name static-context\">Commission</span><span class=\"token operator\">::</span><span class=\"token function\">createPercentage</span><span class=\"token punctuation\">(</span><span class=\"token variable\">$event</span><span class=\"token operator\">-></span><span class=\"token function\">getCommissionValue</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token operator\">/</span> <span class=\"token number\">100</span><span class=\"token punctuation\">)</span>\n            <span class=\"token punctuation\">:</span> <span class=\"token class-name static-context\">Commission</span><span class=\"token operator\">::</span><span class=\"token function\">createFixed</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">[</span><span class=\"token keyword\">new</span> <span class=\"token class-name\">Price</span><span class=\"token punctuation\">(</span><span class=\"token variable\">$shop</span><span class=\"token operator\">-></span><span class=\"token function\">getDefaultCurrency</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span> <span class=\"token variable\">$event</span><span class=\"token operator\">-></span><span class=\"token function\">getCommissionValue</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">]</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n\n        <span class=\"token variable\">$product</span> <span class=\"token operator\">=</span> <span class=\"token keyword\">new</span> <span class=\"token class-name\">Product</span><span class=\"token punctuation\">(</span>\n            <span class=\"token variable\">$sku</span><span class=\"token punctuation\">,</span> <span class=\"token variable\">$baseItem</span><span class=\"token punctuation\">,</span> <span class=\"token variable\">$event</span><span class=\"token operator\">-></span><span class=\"token function\">getName</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span> <span class=\"token constant\">null</span><span class=\"token punctuation\">,</span> <span class=\"token variable\">$shop</span><span class=\"token punctuation\">,</span>\n            <span class=\"token variable\">$event</span><span class=\"token operator\">-></span><span class=\"token function\">getDescription</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span> <span class=\"token variable\">$commission</span><span class=\"token punctuation\">,</span> <span class=\"token variable\">$event</span><span class=\"token operator\">-></span><span class=\"token function\">getCreatedAt</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span>\n        <span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n\n        <span class=\"token variable\">$event</span><span class=\"token operator\">-></span><span class=\"token function\">isDeleted</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token operator\">?</span> <span class=\"token variable\">$product</span><span class=\"token operator\">-></span><span class=\"token function\">delete</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">:</span> <span class=\"token variable\">$product</span><span class=\"token operator\">-></span><span class=\"token function\">restore</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n\n        <span class=\"token variable\">$this</span><span class=\"token operator\">-></span><span class=\"token property\">productRepository</span><span class=\"token operator\">-></span><span class=\"token function\">save</span><span class=\"token punctuation\">(</span><span class=\"token variable\">$product</span><span class=\"token punctuation\">,</span> <span class=\"token constant boolean\">true</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n    <span class=\"token punctuation\">}</span>\n<span class=\"token punctuation\">}</span></code></pre></div>\n<p>In my <a href=\"/blog/strangler-pattern-practical-guide/\">practical guide</a> I gave a simple test for a real ACL: grep your new codebase for the word <em>legacy</em> - every hit outside the ACL is a leak. Running that grep on this project, the hits cluster in <code class=\"language-text\">Infrastructure/</code> - <code class=\"language-text\">LegacyProductApi</code>, <code class=\"language-text\">LegacyProductTranslator</code>, the mapping entities. The <code class=\"language-text\">Domain/Model</code> directory comes back clean. Held for years, that line is the difference between a migration and a reshuffle. The word itself is not the point - extend the test to any legacy concept you deliberately dropped from the new model, and you will not find those either.</p>\n<p>Was it perfect? No. The translator returns an array instead of a typed object - there is literally a <code class=\"language-text\">@todo this should maybe return a Product instance?</code> on the interface, and it stayed a todo for years. If I were building it today, that boundary would be a typed DTO validated field by field. But the <em>placement</em> of the boundary was right, and placement is what you cannot fix later.</p>\n<p>And that is it - the full sync, end to end. Based on the domain events and models, we were able to build a new, fast and efficient product catalogue, and later easily extend it to product pages with pricing info. Unfortunately, the migration was stopped soon after, for business rather than technical reasons - so this time we never got to click \"delete\" on the legacy part the way we did in other projects. More on that in the closing thoughts.</p>\n<style>\n  .responsive-iframe {\n    position: relative;\n    width: 100%;\n    height: 0;\n    padding-bottom: 66.67%; /* Default aspect ratio (320/480) */\n  }\n\n  .responsive-iframe iframe {\n    position: absolute;\n    top: 0;\n    left: 0;\n    width: 100%;\n    height: 100%;\n    border: 1px solid #EEE;\n    background: white;\n  }\n\n  /* Adjust the aspect ratio for larger screens */\n  @media (min-width: 900px) {\n    .responsive-iframe {\n      padding-bottom: 50%; /* Less height on larger screens */\n    }\n  }\n</style>\n<div class=\"responsive-iframe\">\n  <iframe src=\"https://phpatscale.substack.com/embed\" frameborder=\"0\" scrolling=\"no\"></iframe>\n</div>\n<h2>Why simple won</h2>\n<p>Step back and count the moving parts: two queue tables, a dozen triggers, one internal HTTP API, one console command, one cron entry. That is the entire synchronization infrastructure for strangling the product catalog out of a monolith. No broker to operate, no CDC connector to babysit, no schema registry. When sync misbehaved, debugging looked like <code class=\"language-text\">SELECT * FROM shop_item_updated ORDER BY updated_at LIMIT 20</code> and reading a cron log.</p>\n<p>The simplicity was not luck. It was bought by three constraints we chose early and defended:</p>\n<ol>\n<li><strong>One-directional flow.</strong> Legacy writes, new reads. The moment data flows both ways, you are in conflict-resolution territory and none of the code above survives.</li>\n<li><strong>Eventual consistency, explicitly accepted.</strong> Three minutes of staleness was a business decision, made once, with the product owner in the room. Every relaxation like this removes a whole layer of machinery.</li>\n<li><strong>At-least-once plus idempotency</strong>, instead of trying for exactly-once. Duplicates are free when the consumer upserts; lost updates are never free. Design the failure direction, then stop worrying.</li>\n</ol>\n<p>And around the simple pipe, the discipline that actually mattered: a mapper publishing a clean contract on the way out of legacy, and a translator absorbing quirks on the way into the new system, so that after years of syncing, the new domain model still did not know what a <code class=\"language-text\">pattern_is_hidden</code> flag was.</p>\n<p>The sync outlived every estimate we made for it, which is true of most \"temporary\" migration infrastructure - in the guide I called this the <a href=\"/blog/strangler-pattern-practical-guide/\">\"short-lived forever\" state</a>, and this project is where I learned it. Build the temporary thing simple enough that running it for years does not hurt, and clean enough that deleting it is a <code class=\"language-text\">DROP TRIGGER</code> and a <code class=\"language-text\">git rm</code>.</p>\n<h2>FAQ</h2>\n<h3>Why not use Debezium or another CDC tool instead of triggers?</h3>\n<p>For this project, operational cost. Debezium means running Kafka Connect (or at least a Debezium server), monitoring a binlog reader, and handling connector failures - permanent infrastructure for a temporary migration. The trigger approach needed nothing that was not already there. CDC earns its keep when you need ordered event streams with payloads, multiple consumers, or near-real-time latency. For one consumer that tolerates minutes of delay, a queue table is hard to beat. And it served the traffic we had pretty well. </p>\n<h3>Does polling every few minutes scale?</h3>\n<p>Better than intuition suggests, because the queue coalesces. The batch size is bounded by \"products changed since the last run\", not by write volume - a product saved fifty times is still one row. Our batches rarely approached the 200 limit outside of bulk imports, and those simply drained over a few consecutive runs. If your change rate outgrows the interval permanently, that is the signal to move to a streaming approach - but measure first.</p>\n<h3>Why pull from the new side instead of legacy pushing changes?</h3>\n<p>Pull keeps the legacy side passive and nearly untouched - the strangler ideal. A push design means the legacy app must know the new system's address, handle its downtime, implement retries and backoff. With pull, all the intelligence lives in the new codebase, where you actually want to write code, and legacy's only job is to answer two HTTP endpoints. If the new side is down, changes accumulate safely in the queue table until it comes back.</p>\n<h3>Is a database trigger not an antipattern itself?</h3>\n<p>Triggers holding <em>business logic</em> are a well-earned antipattern - invisible, hard to test, painful to debug. Triggers as a <em>change-tracking mechanism</em> are a different animal: a single dumb statement, no decisions, flagging an ID. That is a pragmatic form of change data capture, and it solved the one problem application-level events cannot solve in a legacy system - catching the write paths you do not know about. Keep them dumb, keep them in versioned migrations, document them where developers will look.</p>\n<h3>How do you keep the two systems' IDs from contaminating each other?</h3>\n<p>Dedicated mapping tables, owned by the new system's infrastructure layer. Legacy IDs never appear on domain entities - only in <code class=\"language-text\">*ToLegacyMap</code> tables that pair a legacy identifier with a new SKU. The domain model references only its own identifiers. When the legacy system is finally decommissioned, the mapping tables are dropped and the domain schema does not change at all - which is a good test of whether your ACL is real.</p>\n<h2>Closing thoughts</h2>\n<p>Does this always work? No, it does not, and it should not be your default approach. Triggers, queue tables and cron jobs are one possible answer, chosen for one specific situation: a single writer, a single reader, and a business that could accept a few minutes of delay. What I wanted to show is that good modelling and an open mind are the key to a simple, easy to maintain solution. <strong>When the boundaries are right, the machinery between them can stay boring.</strong></p>\n<p>There is one more reason this project stuck with me. As I mentioned, the migration was eventually stopped - a business decision, nothing to do with the technology. And nothing broke. We did not back out of a half-finished rewrite, nobody scrambled to patch a system stuck between two worlds. We simply stopped, with every strangled piece still running in production and the legacy app still handling the rest. In the <a href=\"/blog/strangler-pattern-practical-guide/\">practical guide</a> I wrote that if you have to pause a strangler migration, you still have a fully working product. This is the project where I saw that safety net catch a real fall.</p>","timeToRead":19,"frontmatter":{"title":"Clean Model Always Wins: A Strangler Pattern Data Sync Case Study","tags":["php","software-development","code-quality","modernization"],"author":"mkurzeja","categories":["for-developers"],"dateUpdated":null,"customTopics":[["The Strangler Pattern - A Practical Guide","/strangler-pattern-practical-guide","blog"],["Strangler Pattern in practice","/strangler-pattern-in-practice","blog"],["PHP Code Quality with PHPStan","/how-to-ensure-php-code-quality-static-analysis-using-phpstan-with-symfony","blog"],["PHP Modernization","https://accesto.com/services/php-refactoring-services/","services"]],"customCTA":["Need help with PHP refactoring?","https://accesto.com/contact/","Talk to us"],"customContactCTA":["Need help with PHP refactoring?","We work on mature PHP systems — performance, modernization, and architecture.","Talk to us","https://accesto.com/contact/"],"customContactCTAIcon":null,"heroImage":{"childImageSharp":{"fluid":{"base64":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAIAAAA7N+mxAAAACXBIWXMAAAsTAAALEwEAmpwYAAAB7UlEQVQoz02RzW7TQBSFbY89Y1BS2zPTum4i2iZtE0KaovIjoCCyKGrFhjqNf+Jx0jalJAoVsAhqs2CF2NEdCyRExZLyBogNEg/CmrfgOqaA9OnqXN05upp7JEL9f7CA0ABTH1MPWb5GA8I80Of46QizAFueZjUl8OgMCP7AkwrjKaeRpVtSpkH+2iwPm00wEx5Cq1FPgl6ZaCBjG1CyjVTLWZezjYVSO1/uI6MJNgJOu4VzMbaaOOsSwyWw2crHiyuDwnK/UO0vrQzmKk/mLj/OlXvtw9Pjk2+NwSfqeMTcQhkX2S10aUeF5auvtNlestkpdm/Vj9bWR9fuDq/fG4Kw53dmqi8e7n24uRoM6huVO4cZcYYWnma8z/TZD6V2nPXPLm5+VCpHkpET08XuzOK+Xejahf186WDCEcb8Qe3+qHx17/aVR8vrw8L7X8qDd+rud/z6p1w/kf0vqP1VWXsrwX9UM0Gz4CQhMnxkBoRvTxfbmZyQjFCaHSjuKSo9l0sv5Rtv0FRH5TGqjlBtJBE4NYfXCToPdd7SucA0Uq1QoxGmguR21aWe5nSw4SdQgVmETUg0ksbxjhPmIWEh5JRULnQeERbpLNaZ0K1E4MkO5kCcQrj4z8zCcU1FKyVx8lifjC9ATQznMEFo9Buae3BDGG76PwAAAABJRU5ErkJggg==","aspectRatio":1.9736842105263157,"src":"/blog/static/c7307a4ce77c9f6de8cb9ac9a4b2e93d/f3583/strangler_pattern_data_sync.png","srcSet":"/blog/static/c7307a4ce77c9f6de8cb9ac9a4b2e93d/630fb/strangler_pattern_data_sync.png 300w,\n/blog/static/c7307a4ce77c9f6de8cb9ac9a4b2e93d/2a4de/strangler_pattern_data_sync.png 600w,\n/blog/static/c7307a4ce77c9f6de8cb9ac9a4b2e93d/f3583/strangler_pattern_data_sync.png 1200w,\n/blog/static/c7307a4ce77c9f6de8cb9ac9a4b2e93d/7adbf/strangler_pattern_data_sync.png 1660w","sizes":"(max-width: 1200px) 100vw, 1200px"}},"publicURL":"/blog/static/c7307a4ce77c9f6de8cb9ac9a4b2e93d/strangler_pattern_data_sync.png"}},"fields":{"date":"2026-09-21"}},"authorImage":{"childImageSharp":{"fluid":{"base64":"data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAUABQDASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAAAAQF/8QAFgEBAQEAAAAAAAAAAAAAAAAAAgAB/9oADAMBAAIQAxAAAAHakshK0gjHKMUiz//EABoQAAMBAAMAAAAAAAAAAAAAAAECAwARMTL/2gAIAQEAAQUCckBGqtM/mTBq6rnnphRt/8QAFxEBAAMAAAAAAAAAAAAAAAAAEAExQf/aAAgBAwEBPwHCaP/EABYRAQEBAAAAAAAAAAAAAAAAABABMf/aAAgBAgEBPwEmn//EABwQAAEEAwEAAAAAAAAAAAAAAAEAAhAhESIxQf/aAAgBAQAGPwLXqAe4OBgryMKqj//EABsQAQADAAMBAAAAAAAAAAAAAAEAESEQMUFh/9oACAEBAAE/Ic+01vkwePhVcd5LCVcbG48BRwlDpF8lLxn/2gAMAwEAAgADAAAAEJvPAv/EABcRAQEBAQAAAAAAAAAAAAAAAAERABD/2gAIAQMBAT8QAp4msLN//8QAFxEBAQEBAAAAAAAAAAAAAAAAAREAEP/aAAgBAgEBPxBW8BGQu//EABwQAQEAAgIDAAAAAAAAAAAAAAERADEQIVFhcf/aAAgBAQABPxAjFiOnTbiUxdk5b864nqAqeTBq5t2poOECEYpvWIZpWmVm8PK+xM//2Q==","aspectRatio":1,"src":"/blog/static/e68dd4372e7cd1a7a108492da7836f1a/ea649/mkurzeja.jpg","srcSet":"/blog/static/e68dd4372e7cd1a7a108492da7836f1a/b1092/mkurzeja.jpg 11w,\n/blog/static/e68dd4372e7cd1a7a108492da7836f1a/1d7ec/mkurzeja.jpg 21w,\n/blog/static/e68dd4372e7cd1a7a108492da7836f1a/ea649/mkurzeja.jpg 42w,\n/blog/static/e68dd4372e7cd1a7a108492da7836f1a/1c2ab/mkurzeja.jpg 63w,\n/blog/static/e68dd4372e7cd1a7a108492da7836f1a/567fa/mkurzeja.jpg 84w,\n/blog/static/e68dd4372e7cd1a7a108492da7836f1a/79880/mkurzeja.jpg 412w","sizes":"(max-width: 42px) 100vw, 42px"}}}},"pageContext":{"slug":"/clean-model-always-wins-strangler-pattern-case-study/","authorImagePath":"mkurzeja.jpg"}},"staticQueryHashes":["1607616503","473848531"]}