{"id":313,"date":"2026-04-23T07:02:17","date_gmt":"2026-04-23T07:02:17","guid":{"rendered":"https:\/\/magendoo.ro\/insights\/?p=313"},"modified":"2026-04-23T07:02:17","modified_gmt":"2026-04-23T07:02:17","slug":"why-most-agencies-build-monoliths-even-when-they-say-architecture","status":"publish","type":"post","link":"https:\/\/magendoo.ro\/insights\/why-most-agencies-build-monoliths-even-when-they-say-architecture\/","title":{"rendered":"Why Most Agencies Build Monoliths (Even When They Say Architecture)"},"content":{"rendered":"<p>The agency said \u201cclean architecture.\u201d The codebase said otherwise. Sixteen custom modules, three overridden core controllers, a service layer that talks directly to the database, and a Cron job that synchronizes ERP data by calling a third-party API from inside a Magento observer. Architecture? Sure \u2014 just not the kind anyone intended.<\/p>\n<p>This isn\u2019t a story about incompetent developers. It\u2019s a story about incentives.<\/p>\n<h2 class=\"wp-block-heading\">The Incentive Problem Is the Architecture Problem<\/h2>\n<p>Before blaming the code, look at the contract.<\/p>\n<p>Most agency projects are fixed-scope, fixed-price. The agency wins the deal by promising a specific set of features at a specific cost. Every hour spent designing integration layers, defining service boundaries, or building proper middleware is an hour that doesn\u2019t appear in the deliverable list. It comes out of margin.<\/p>\n<p>So what happens? Everything goes into Magento. Observers become integration endpoints. Plugins become business logic containers. The codebase becomes a single organism where changing one thing requires understanding all of it.<\/p>\n<p>The agency doesn\u2019t set out to build a monolith. They set out to ship a project within a budget. The monolith is a side effect of how the budget works.<\/p>\n<p>This is the first uncomfortable truth about agency architecture: the technical decisions are financial decisions in disguise.<\/p>\n<h2 class=\"wp-block-heading\">What \u201cEverything in Magento\u201d Actually Costs<\/h2>\n<p>When all business logic lives inside Magento, you\u2019ve made a specific trade:<\/p>\n<ul>\n<li><strong>Short-term:<\/strong> faster to deliver, easier to demo, less infrastructure to explain to the client<\/li>\n<li><strong>Long-term:<\/strong> Magento upgrade = business logic upgrade. Every patch version is a risk assessment.<\/li>\n<\/ul>\n<p>The concrete costs appear 18\u201324 months post-launch:<\/p>\n<ul>\n<li>A Magento security patch breaks a custom observer that was intercepting core payment flow<\/li>\n<li>An ERP vendor changes their API; the integration point is buried in a plugin that three other plugins depend on<\/li>\n<li>The client wants to add a new storefront; the entire backend logic is Magento-specific and can\u2019t be reused<\/li>\n<\/ul>\n<p>The irony is that clients often don\u2019t see these costs as architecture failures. They see them as normal maintenance. And agencies are often still on the hook for that maintenance \u2014 reinforcing the pattern.<\/p>\n<h2 class=\"wp-block-heading\">The Integration Expertise Gap<\/h2>\n<p>Part of the problem is skill set. Most Magento developers are exceptional at Magento. They know DI, service contracts, plugins, observers, cron, GraphQL resolvers. What they\u2019re often less comfortable with is building outside Magento: standalone PHP services, Go microservices, message queues, webhook processing infrastructure.<\/p>\n<p>So when a project requires synchronizing with an ERP, the path of least resistance is:<\/p>\n<div class=\"sourceCode\" id=\"cb1\">\n<pre class=\"sourceCode php\"><code class=\"sourceCode php\"><span id=\"cb1-1\"><a href=\"#cb1-1\" aria-hidden=\"true\" tabindex=\"-1\"><\/a><span class=\"co\">\/\/ In a Cron job inside Magento<\/span><\/span>\n<span id=\"cb1-2\"><a href=\"#cb1-2\" aria-hidden=\"true\" tabindex=\"-1\"><\/a><span class=\"kw\">class<\/span> SyncOrders<\/span>\n<span id=\"cb1-3\"><a href=\"#cb1-3\" aria-hidden=\"true\" tabindex=\"-1\"><\/a>{<\/span>\n<span id=\"cb1-4\"><a href=\"#cb1-4\" aria-hidden=\"true\" tabindex=\"-1\"><\/a>    <span class=\"kw\">public<\/span> <span class=\"kw\">function<\/span> execute()<span class=\"ot\">:<\/span> <span class=\"dt\">void<\/span><\/span>\n<span id=\"cb1-5\"><a href=\"#cb1-5\" aria-hidden=\"true\" tabindex=\"-1\"><\/a>    {<\/span>\n<span id=\"cb1-6\"><a href=\"#cb1-6\" aria-hidden=\"true\" tabindex=\"-1\"><\/a>        <span class=\"va\">$orders<\/span> <span class=\"op\">=<\/span> <span class=\"va\">$this<\/span>-&gt;orderRepository-&gt;getList(<span class=\"va\">$criteria<\/span>)<span class=\"ot\">;<\/span><\/span>\n<span id=\"cb1-7\"><a href=\"#cb1-7\" aria-hidden=\"true\" tabindex=\"-1\"><\/a>        <span class=\"cf\">foreach<\/span> (<span class=\"va\">$orders<\/span> <span class=\"kw\">as<\/span> <span class=\"va\">$order<\/span>) {<\/span>\n<span id=\"cb1-8\"><a href=\"#cb1-8\" aria-hidden=\"true\" tabindex=\"-1\"><\/a>            <span class=\"va\">$this<\/span>-&gt;erpClient-&gt;pushOrder(<span class=\"va\">$order<\/span>)<span class=\"ot\">;<\/span><\/span>\n<span id=\"cb1-9\"><a href=\"#cb1-9\" aria-hidden=\"true\" tabindex=\"-1\"><\/a>        }<\/span>\n<span id=\"cb1-10\"><a href=\"#cb1-10\" aria-hidden=\"true\" tabindex=\"-1\"><\/a>    }<\/span>\n<span id=\"cb1-11\"><a href=\"#cb1-11\" aria-hidden=\"true\" tabindex=\"-1\"><\/a>}<\/span><\/code><\/pre>\n<\/div>\n<p>This works. It ships. It even passes code review. But it\u2019s a wrong boundary decision \u2014 Magento now owns the ERP sync logic, the ERP client configuration, the retry behavior, and the failure surface.<\/p>\n<p>The alternative \u2014 a dedicated sync service that reads Magento orders via API and pushes to ERP independently \u2014 requires comfort with building services outside the Magento container. That\u2019s a different skill, and it\u2019s not the skill most Magento teams are hired for.<\/p>\n<h2 class=\"wp-block-heading\">Patterns That Create Accidental Monoliths<\/h2>\n<p>These are the patterns I keep seeing in audits:<\/p>\n<p><strong>Observer chains as integration pipelines.<\/strong> An observer fires on <code>checkout_submit_all_after<\/code>. It calls a loyalty API. Another observer on the same event calls a CRM. A third calls a tax service. Each works fine independently. Together they create a request pipeline that blocks checkout if any external service is slow.<\/p>\n<p><strong>Plugins that own domain logic.<\/strong> A plugin on <code>OrderRepository::save()<\/code> validates business rules that should live in the order creation flow. Now <code>OrderRepository<\/code> is load-bearing for business logic it was never designed to carry.<\/p>\n<p><strong>Custom modules as mini-apps.<\/strong> A single module handles product import, price sync, inventory update, and fulfillment routing. It has its own database tables, its own cron jobs, its own configuration section. It\u2019s not a Magento module \u2014 it\u2019s an application that happens to live inside Magento.<\/p>\n<p><strong>API integrations via cron.<\/strong> Instead of event-driven webhooks or message queues, data moves between systems on 15-minute cron cycles. This isn\u2019t an architecture decision; it\u2019s the default when no one designs the integration layer.<\/p>\n<h2 class=\"wp-block-heading\">The Boundary Model Agencies Avoid<\/h2>\n<p>A clear system boundary would look like this:<\/p>\n<ul>\n<li><strong>Magento<\/strong> handles: catalog, pricing, checkout, customer accounts, order state machine<\/li>\n<li><strong>Integration layer<\/strong> handles: ERP sync, external API calls, webhook processing, data transformation<\/li>\n<li><strong>External systems<\/strong> handle: fulfillment, loyalty, CRM, tax calculation<\/li>\n<\/ul>\n<p>The integration layer can be as simple as a dedicated PHP service or as sophisticated as a Go microservice with a message queue. The point is that it exists as a separate deployment unit with its own failure domain.<\/p>\n<p>This boundary matters because:<\/p>\n<ul>\n<li>Magento upgrades don\u2019t break ERP integration logic<\/li>\n<li>External API failures don\u2019t crash checkout<\/li>\n<li>The integration layer can scale independently<\/li>\n<li>Different teams can own different boundaries<\/li>\n<\/ul>\n<p>Agencies avoid this pattern because it adds infrastructure. Another server, another deployment pipeline, another thing to explain to the client. In a fixed-price contract, that infrastructure has no line item.<\/p>\n<h2 class=\"wp-block-heading\">Decision Framework<\/h2>\n<p><strong>Build inside Magento when:<\/strong> &#8211; The logic is genuinely part of the commerce flow (pricing, cart rules, checkout steps) &#8211; It uses Magento data that isn\u2019t available via API without significant overhead &#8211; The logic changes with Magento version and should stay coupled<\/p>\n<p><strong>Build outside Magento when:<\/strong> &#8211; You\u2019re calling external APIs that could be slow or unavailable &#8211; The logic involves data transformation or routing between systems &#8211; The functionality needs to scale independently of Magento &#8211; Multiple systems (not just Magento) will need the same capability &#8211; You\u2019re building anything that resembles a pipeline or workflow engine<\/p>\n<p><strong>Red flags that signal a boundary violation:<\/strong> &#8211; An observer that makes external API calls &#8211; A cron job that queries Magento AND an external system &#8211; A plugin that owns retry logic for external service calls &#8211; A custom module with its own database tables for non-commerce data<\/p>\n<h2 class=\"wp-block-heading\">The Leadership Question<\/h2>\n<p>As a tech lead, the question is not \u201cCan we build this inside Magento?\u201d \u2014 the answer is almost always yes. The question is \u201cWhat is the upgrade cost if we do?\u201d Every piece of business logic inside Magento is logic you\u2019ll need to verify, test, and potentially rewrite with every major version. A Magento 2.4.4 to 2.4.7 upgrade should not require rearchitecting your ERP integration.<\/p>\n<p>When you accept a project with a fixed budget and no line item for an integration layer, you\u2019re making an implicit decision that your client will pay the technical debt later. Sometimes that\u2019s the right trade. But it should be a conscious decision, not the default.<\/p>\n<p>Push back on scope. Document the boundary decisions. If the integration layer doesn\u2019t fit in the budget, tell the client what they\u2019re trading. That conversation is harder. It\u2019s also the only way to stop building the same monolith on every project.<\/p>\n<h2 class=\"wp-block-heading\">Conclusion<\/h2>\n<p>Most agencies don\u2019t build monoliths because they don\u2019t know better. They build monoliths because fixed-price contracts reward shipping over designing. The incentive structure produces the architecture.<\/p>\n<p>If you want to build differently, you have to change what you optimize for. Charge for architecture decisions. Build integration boundaries as first-class deliverables. Make the case that an integration layer is cheaper than a Magento upgrade gone wrong.<\/p>\n<p>The agencies that figure this out stop losing clients after year two. They become the people clients call to fix what the previous agency built. That\u2019s not an accident \u2014 it\u2019s what happens when you treat architecture as a service, not a side effect.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>The agency said \u201cclean architecture.\u201d The codebase said otherwise. Sixteen custom modules, three overridden core controllers, a service layer that talks directly to the database, and a Cron job that synchronizes ERP data by calling a third-party API from inside a Magento observer. Architecture? Sure \u2014 just not the kind anyone intended. This isn\u2019t a [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":312,"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-313","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\/313","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=313"}],"version-history":[{"count":1,"href":"https:\/\/magendoo.ro\/insights\/wp-json\/wp\/v2\/posts\/313\/revisions"}],"predecessor-version":[{"id":314,"href":"https:\/\/magendoo.ro\/insights\/wp-json\/wp\/v2\/posts\/313\/revisions\/314"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/magendoo.ro\/insights\/wp-json\/wp\/v2\/media\/312"}],"wp:attachment":[{"href":"https:\/\/magendoo.ro\/insights\/wp-json\/wp\/v2\/media?parent=313"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/magendoo.ro\/insights\/wp-json\/wp\/v2\/categories?post=313"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/magendoo.ro\/insights\/wp-json\/wp\/v2\/tags?post=313"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}