<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sandeep Singh</title>
    <description>The latest articles on DEV Community by Sandeep Singh (@aidevbuilds).</description>
    <link>https://dev.to/aidevbuilds</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3984726%2F0d151670-f4c2-4f2d-9e95-49bd50360cdc.png</url>
      <title>DEV Community: Sandeep Singh</title>
      <link>https://dev.to/aidevbuilds</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aidevbuilds"/>
    <language>en</language>
    <item>
      <title>Stop Building Microservices on Day One. Build a "Disposable" Monolith Instead.</title>
      <dc:creator>Sandeep Singh</dc:creator>
      <pubDate>Sat, 11 Jul 2026 11:34:52 +0000</pubDate>
      <link>https://dev.to/aidevbuilds/stop-building-microservices-on-day-one-build-a-disposable-monolith-instead-53kf</link>
      <guid>https://dev.to/aidevbuilds/stop-building-microservices-on-day-one-build-a-disposable-monolith-instead-53kf</guid>
      <description>&lt;p&gt;Remember 2018? If you pitched a new project back then without a Kubernetes cluster, a service mesh, and a dozen-plus microservices, you were practically laughed out of the architecture review. We were promised independent deployments, infinite scalability, and a future with no technical debt just clean little services, each minding its own business.&lt;/p&gt;

&lt;p&gt;Fast forward to today, and the hangover has set in. Teams are drowning in distributed tracing dashboards just to find out why a request takes 4 seconds. Engineers are chasing a single bug across five repositories, three on-call rotations, and one very confused Slack thread. And a lot of these systems are serving a few thousand users not the millions they were architected for. The industry is slowly admitting what a few people were saying all along: jumping straight into a distributed system on Day One is usually a mistake, not a sign of maturity.&lt;/p&gt;

&lt;p&gt;But the fear of the alternative hasn't gone anywhere. Nobody wants to go back to the "big ball of mud" the kind of monolith where touching the payment logic somehow breaks the notification engine, and nobody's entirely sure why.&lt;/p&gt;

&lt;p&gt;Now add AI coding assistants to this picture, and things get messier in a new way.&lt;/p&gt;

&lt;p&gt;Point an AI assistant at a sprawling microservices estate, and it struggles not because it's "afraid" of complexity, but because it simply can't hold a dozen services' worth of contracts, queues, and network boundaries in its context window at once. It'll guess. It'll miss a contract. It'll confidently change a field name without realizing three other services depend on it.&lt;/p&gt;

&lt;p&gt;Point that same assistant at a traditional, unstructured monolith, and it has the opposite problem: too much access, not too little. It'll reach across folders that have nothing to do with each other. It'll write a SQL join straight across two domains because the tables happened to be sitting right there. Ask it to "add a discount field," and it might just add a foreign key from &lt;strong&gt;&lt;code&gt;orders&lt;/code&gt;&lt;/strong&gt; into &lt;strong&gt;&lt;code&gt;users&lt;/code&gt;&lt;/strong&gt; because, technically, that's the fastest way to make the test pass. AI doesn't think about architectural integrity. It thinks about making the current diff green.&lt;/p&gt;

&lt;p&gt;So we're stuck between two failure modes: microservices are too complex too early, and monoliths at least the kind most of us inherit are a breeding ground for exactly the kind of debt AI is good at generating quickly.&lt;/p&gt;

&lt;p&gt;The fix isn't to pick the lesser evil. It's to build something in between: a &lt;strong&gt;Modular Monolith&lt;/strong&gt;, structured with &lt;strong&gt;Clean Architecture&lt;/strong&gt;. Enforce real, physical boundaries inside one codebase, and you get the operational simplicity of a monolith today, a tightly fenced sandbox where AI can actually do good work, and if you ever need it a much shorter path to pulling a piece out as its own service.&lt;/p&gt;

&lt;p&gt;Here's how to build one that's designed to be taken apart.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Root Mistake: Your Database &lt;em&gt;Is&lt;/em&gt; Your Architecture
&lt;/h2&gt;

&lt;p&gt;You can draw the cleanest module diagram in the world. Boxes for &lt;strong&gt;&lt;code&gt;Orders&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;Inventory&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;Billing&lt;/code&gt;&lt;/strong&gt;. Arrows pointing the right way. It'll look great in Confluence.&lt;/p&gt;

&lt;p&gt;None of it matters if your database doesn't agree with the diagram.&lt;/p&gt;

&lt;p&gt;This is the part teams get wrong most often: they enforce boundaries in their &lt;em&gt;folder structure&lt;/em&gt; and ignore them completely in their &lt;em&gt;schema&lt;/em&gt;. A repository in the &lt;strong&gt;&lt;code&gt;Orders&lt;/code&gt;&lt;/strong&gt; module reaches directly into the &lt;strong&gt;&lt;code&gt;inventory_items&lt;/code&gt;&lt;/strong&gt; table because, well, it needed the stock count and writing a join was faster than calling another module's interface. Multiplied across a few dozen tickets, you end up with a &lt;strong&gt;&lt;code&gt;users&lt;/code&gt;&lt;/strong&gt; table that fifteen unrelated modules read from and write to directly no interface, no contract, just shared mutable state with extra steps.&lt;/p&gt;

&lt;p&gt;Here's the test that actually tells you whether your architecture is real: &lt;strong&gt;stop looking at your folder names, and look at your foreign keys.&lt;/strong&gt; Your schema is the most honest documentation your system has. It doesn't care what the diagram says.&lt;/p&gt;

&lt;p&gt;This is also exactly where "extract this into a microservice" projects go to die. The code-level coupling is usually fixable in days. Untangling years of cross-domain joins and shared tables figuring out who actually &lt;em&gt;owns&lt;/em&gt; a piece of data, migrating it, and rewriting every query that assumed it could just reach across is the part that turns a two-week extraction into a six-month death march.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fasywxzkh9b5dnxjpgc1t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fasywxzkh9b5dnxjpgc1t.png" alt=" " width="799" height="436"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Fix: Bounded Contexts as the Real Unit of Modularity
&lt;/h2&gt;

&lt;p&gt;Borrow a term from Domain-Driven Design here, because it's the right one: a &lt;strong&gt;bounded context&lt;/strong&gt;. Not a layer. Not a folder. A bounded context is a slice of your domain &lt;strong&gt;&lt;code&gt;Orders&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;Billing&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;Inventory&lt;/code&gt;&lt;/strong&gt; that owns its own data and exposes everything else through an explicit interface: a method call, a published event, a defined contract. Nothing else.&lt;/p&gt;

&lt;p&gt;The rule is simple to state and uncomfortable to enforce: &lt;strong&gt;no module touches another module's tables. Ever.&lt;/strong&gt; Not through a shared ORM model, not through a "quick" join, not through a view that quietly bridges two schemas. If &lt;strong&gt;&lt;code&gt;Orders&lt;/code&gt;&lt;/strong&gt; needs something from &lt;strong&gt;&lt;code&gt;Inventory&lt;/code&gt;&lt;/strong&gt;, it asks through code, not through SQL.&lt;/p&gt;

&lt;p&gt;If this sounds suspiciously like microservices without the network calls, that's because it basically is. You're choosing to pay the &lt;em&gt;organizational&lt;/em&gt; cost of real boundaries now, while deliberately deferring the &lt;em&gt;operational&lt;/em&gt; cost of microservices deployment pipelines, service discovery, network failure handling until you actually have a reason to pay it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# inventory/domain/ports.py
# The ONLY way another module is allowed to ask Inventory for anything.
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;abc&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ABC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;abstractmethod&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;


&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StockLevel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;available_quantity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;InventoryQueryPort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ABC&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Public contract for the Inventory bounded context.
    Other modules depend on THIS, never on Inventory&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s internals.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="nd"&gt;@abstractmethod&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_stock_level&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;StockLevel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# orders/application/place_order.py
# Orders depends on the Inventory PORT, not on Inventory's database,
# models, or internal services. It doesn't even know Inventory uses Postgres.
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;inventory.domain.ports&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;InventoryQueryPort&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PlaceOrderUseCase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inventory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;InventoryQueryPort&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_inventory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;inventory&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;stock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_inventory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_stock_level&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;stock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;available_quantity&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
        &lt;span class="c1"&gt;# ... proceed with order creation
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice what &lt;strong&gt;&lt;code&gt;PlaceOrderUseCase&lt;/code&gt;&lt;/strong&gt; &lt;em&gt;can't&lt;/em&gt; do: it can't run a query against Inventory's tables, because it never imported anything that would let it. The boundary isn't a convention written in a wiki page somewhere it's a Python import that simply doesn't exist.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpsimi8iyism32ba7hwhg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpsimi8iyism32ba7hwhg.png" alt=" " width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Clean Architecture Inside Each Module
&lt;/h2&gt;

&lt;p&gt;The previous section was about boundaries &lt;em&gt;between&lt;/em&gt; modules. This one's about structure &lt;em&gt;inside&lt;/em&gt; each one and this is where Clean Architecture's actual core idea earns its keep: &lt;strong&gt;the dependency arrow always points inward.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Your business rules the actual logic of "what does placing an order mean" sit at the center, and they know nothing about Postgres, FastAPI, or any other framework. The outer layers your database adapter, your web framework, your ORM depend on the core. The core never depends on them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# orders/domain/entities.py
# Pure business logic. No imports from FastAPI, SQLAlchemy, or anything
# that knows what a database or HTTP request even is.
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;enum&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Enum&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Enum&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;PENDING&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pending&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;CONFIRMED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;confirmed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;REJECTED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rejected&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;OrderStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;OrderStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PENDING&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;confirm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;quantity&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cannot confirm an order with zero quantity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;OrderStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CONFIRMED&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# orders/application/ports.py
# The use case depends on an ABSTRACTION of persistence,
# not a concrete database.
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;abc&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ABC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;abstractmethod&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;orders.domain.entities&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Order&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderRepository&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ABC&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nd"&gt;@abstractmethod&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# orders/infrastructure/postgres_order_repository.py
# The concrete implementation lives on the OUTSIDE,
# and depends INWARD on the port. The core never imports this file.
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;psycopg2&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;orders.application.ports&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OrderRepository&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;orders.domain.entities&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Order&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PostgresOrderRepository&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OrderRepository&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;psycopg2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;extensions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;connection&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INSERT INTO orders (sku, quantity, status) VALUES (%s, %s, %s)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The payoff is concrete, not academic: swap Postgres for DynamoDB, or FastAPI for Flask, and &lt;strong&gt;&lt;code&gt;Order&lt;/code&gt;&lt;/strong&gt; and &lt;strong&gt;&lt;code&gt;PlaceOrderUseCase&lt;/code&gt;&lt;/strong&gt; don't change a single line. More importantly for this article's whole premise when it's time to extract &lt;strong&gt;&lt;code&gt;Orders&lt;/code&gt;&lt;/strong&gt; into its own service, its core logic already doesn't know it's living inside a monolith. You're not rewriting business rules. You're rewriting an adapter.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fggf42g60me6nknita2p8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fggf42g60me6nknita2p8.png" alt=" " width="799" height="436"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Where AI Thrives and Where It Quietly Breaks Your Boundaries
&lt;/h2&gt;

&lt;p&gt;This is where the two threads of this article meet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inside a bounded context, AI is genuinely excellent.&lt;/strong&gt; Ask it to generate a new use case, write a repository implementation, scaffold tests for &lt;strong&gt;&lt;code&gt;PlaceOrderUseCase&lt;/code&gt;&lt;/strong&gt; it'll do it fast, and because the blast radius is one module with a handful of files, even its mistakes are cheap to catch and fix. This is AI at its best: a narrow, well-defined sandbox with a clear contract at the edges.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Across bounded contexts, AI has no idea where your boundaries are unless something physically stops it.&lt;/strong&gt; Ask an assistant to "show the user's order history along with their current loyalty points," and a model with no architectural constraints will often do the statistically obvious thing: reach into both tables in one query, because that's the shortest path to a green test. It's not being careless. It's optimizing for "make this work," and nothing in a typical prompt or repo told it that working &lt;em&gt;isn't&lt;/em&gt; the only requirement.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# What an unconstrained AI assistant will often generate when asked to
# "show order history with loyalty points" a direct cross-module join:
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_order_history_with_points&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# orders and loyalty_points are owned by TWO DIFFERENT bounded contexts.
&lt;/span&gt;    &lt;span class="c1"&gt;# This works. It also quietly deletes your boundary.
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        SELECT o.id, o.sku, o.quantity, lp.points_balance
        FROM orders o
        JOIN loyalty_points lp ON lp.user_id = o.user_id
        WHERE o.user_id = %s
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# The corrected version Orders asks Loyalty through its public port,
# the only crossing point that's allowed to exist:
&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;loyalty.domain.ports&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;LoyaltyQueryPort&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;orders.domain.ports&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OrderRepository&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GetOrderHistoryWithPoints&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;OrderRepository&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;loyalty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;LoyaltyQueryPort&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_orders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_loyalty&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;loyalty&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;history&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_by_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;points&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_loyalty&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_balance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orders&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;history&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;loyalty_points&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;points&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1vbiduxfynzzt677lpl5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1vbiduxfynzzt677lpl5.png" alt=" " width="799" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Same outcome for the end user. Completely different architectural reality. The second version is slightly more code and it's the only version that survives the day you decide &lt;strong&gt;&lt;code&gt;Loyalty&lt;/code&gt;&lt;/strong&gt; needs to become its own service.&lt;/p&gt;

&lt;p&gt;The thesis for this section, stated plainly: &lt;strong&gt;AI is a fantastic resident inside a bounded context, and a terrible architect of the boundaries between them because nothing about how these models work makes them optimize for a constraint you didn't enforce.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A few ways to make that enforcement real, not aspirational:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Import linting&lt;/strong&gt; tools like &lt;strong&gt;&lt;code&gt;import-linter&lt;/code&gt;&lt;/strong&gt; (Python) or dependency-cruiser (JS/TS) can fail a build the moment &lt;strong&gt;&lt;code&gt;orders/&lt;/code&gt;&lt;/strong&gt; imports anything from &lt;strong&gt;&lt;code&gt;inventory/internal/&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;CI as the actual boundary&lt;/strong&gt;, not the README. If the only thing stopping a cross-module import is a sentence in a wiki page, assume it will eventually be crossed by a human or a model.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Make the port the only thing that's importable.&lt;/strong&gt; If a module's internals simply aren't exposed outside its own package, an AI assistant can't accidentally reach for them there's nothing there to reach for.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Payoff: What This Buys You When It's Time to Split
&lt;/h2&gt;

&lt;p&gt;Here's the moment the title promised. Say &lt;strong&gt;&lt;code&gt;Orders&lt;/code&gt;&lt;/strong&gt; has outgrown the monolith maybe it needs to scale independently, maybe a separate team owns it now. Because its core logic never depended on anything outside its own boundary, extraction looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Before: in-process call, still inside the monolith
&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PlaceOrderUseCase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inventory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;InventoryQueryPort&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_inventory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;inventory&lt;/span&gt;  &lt;span class="c1"&gt;# in-process implementation
&lt;/span&gt;
&lt;span class="c1"&gt;# After: extracted to its own service ONLY the adapter changed
&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PlaceOrderUseCase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inventory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;InventoryQueryPort&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_inventory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;inventory&lt;/span&gt;  &lt;span class="c1"&gt;# now an HTTP/gRPC client implementing
&lt;/span&gt;                                       &lt;span class="c1"&gt;# the exact same port interface
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;PlaceOrderUseCase&lt;/code&gt;&lt;/strong&gt; doesn't change. &lt;strong&gt;&lt;code&gt;Order&lt;/code&gt;&lt;/strong&gt;, the entity, doesn't change. The only thing that changes is what sits on the other side of &lt;strong&gt;&lt;code&gt;InventoryQueryPort&lt;/code&gt;&lt;/strong&gt; an in-process Python object yesterday, an HTTP client today.&lt;/p&gt;

&lt;p&gt;Worth being honest about what this &lt;em&gt;doesn't&lt;/em&gt; solve. You're still going to deal with distributed transactions, network failures, and the cost of running another service in production. This approach doesn't make those problems disappear it just defers them to the point where you've actually earned the need to solve them, instead of solving them speculatively on day one.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3at2iaj0bsewp39bk54u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3at2iaj0bsewp39bk54u.png" alt=" " width="799" height="436"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing: Boundaries Are a Discipline, Not a Diagram
&lt;/h2&gt;

&lt;p&gt;The team that says "modular monolith" and actually means it isn't the one with the nicest box-and-arrow diagram. It's the one whose database schema, codebase, and CI pipeline all agree on where the boundaries are and enforce them the same way, whether the next pull request comes from a human or a model.&lt;/p&gt;

&lt;p&gt;AI assistants don't change that calculus. They multiply it. Point one at a codebase with real boundaries, and it'll write fast, contained, disposable code inside them all day. Point one at a codebase without them, and it'll write the cross-domain shortcut just as fast and just as confidently.&lt;/p&gt;

&lt;p&gt;The architecture decides which one you get.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is, in a lot of ways, the architectural sibling of the AI technical debt I wrote about in my last piece debt you pay down once, structurally, instead of forever, one code review at a time.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Enjoyed this breakdown? I write about architecture decisions like this modular monoliths, bounded contexts, and where AI helps vs. where it quietly breaks things in my newsletter, &lt;strong&gt;AI + Architecture&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://newsletter.aidevbuilds.com/subscribe" rel="noopener noreferrer"&gt;Subscribe here →&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>systemdesign</category>
      <category>microservices</category>
    </item>
    <item>
      <title>AI Ships Your Code in Minutes. Your Team Pays for It for Months. Here's Why.</title>
      <dc:creator>Sandeep Singh</dc:creator>
      <pubDate>Mon, 15 Jun 2026 05:47:31 +0000</pubDate>
      <link>https://dev.to/aidevbuilds/ai-ships-your-code-in-minutes-your-team-pays-for-it-for-months-heres-why-30oo</link>
      <guid>https://dev.to/aidevbuilds/ai-ships-your-code-in-minutes-your-team-pays-for-it-for-months-heres-why-30oo</guid>
      <description>&lt;h2&gt;
  
  
  AI Writes Code Fast. That's Exactly the Problem.
&lt;/h2&gt;

&lt;p&gt;Speed is not the enemy. Unmaintainable speed is.&lt;/p&gt;

&lt;p&gt;AI coding assistants can ship a working endpoint in minutes. What they can't do by default is ship one you can still safely touch six months later.&lt;/p&gt;

&lt;p&gt;I've seen this pattern repeatedly. Teams move fast, ship fast, celebrate fast. Then the codebase becomes a place people are afraid of. Every change breaks something unrelated. No one wants to be the one who touched it last.&lt;/p&gt;

&lt;p&gt;The cause is almost never complexity. It's coupling.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the AI Actually Hands You
&lt;/h2&gt;

&lt;p&gt;Ask any AI assistant to build an order creation endpoint. Here's what comes back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/orders&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;customer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SELECT * FROM customers WHERE id = ?&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;creditLimit&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Order rejected: credit limit exceeded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;INSERT INTO orders (customer_id, amount) VALUES (?, ?)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Order created&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works. It'll pass a demo. The PM will be happy.&lt;/p&gt;

&lt;p&gt;Now look at what's jammed into one function: HTTP handling, raw SQL, business rule validation, and response formatting. One file. No boundaries. No separation.&lt;/p&gt;

&lt;p&gt;That's tight coupling. And tight coupling is a time bomb with a slow fuse.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Waiter Who Does Everything
&lt;/h2&gt;

&lt;p&gt;Picture a restaurant where the waiter takes your order, sprints to the pantry, cooks the food, washes the dishes, and tracks inventory.&lt;/p&gt;

&lt;p&gt;With five tables, it holds together. Barely.&lt;/p&gt;

&lt;p&gt;With fifty tables, orders get dropped. Mistakes compound. Nobody knows who's responsible. Training a new person is nearly impossible because one person owns everything.&lt;/p&gt;

&lt;p&gt;Now picture a well-run kitchen. The waiter handles the table. The chef runs the kitchen. The pantry staff manages ingredients. Each role has a clear boundary. Each person can be replaced, trained, and scaled independently.&lt;/p&gt;

&lt;p&gt;That's what layered architecture does for your codebase. Same principle. Different medium.&lt;/p&gt;




&lt;h2&gt;
  
  
  Three Layers. Three Responsibilities.
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Layer 1: The Controller
&lt;/h3&gt;

&lt;p&gt;The controller handles HTTP. That's its only job.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// controllers/orderController.js&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;orderService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It receives the request. It calls a service. It returns a response.&lt;/p&gt;

&lt;p&gt;What it never does: write SQL, enforce business rules, or touch the database. The moment a controller starts deciding whether an order should be approved, it has crossed a boundary it doesn't own.&lt;/p&gt;

&lt;p&gt;Controllers are translators. HTTP in, HTTP out. Nothing else.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 2: The Service Layer
&lt;/h3&gt;

&lt;p&gt;This is where the business logic lives. Pricing rules, credit checks, discount logic, approval workflows all of it belongs here.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// services/orderService.js&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orderData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;customer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;customerRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orderData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;creditLimit&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;orderData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CreditLimitExceededError&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;orderRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orderData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One question drives this layer: How should the business behave?&lt;/p&gt;

&lt;p&gt;Not: &lt;em&gt;How does the database work? That's someone else's job.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Notice the credit limit check throws a domain error not an HTTP status code. The service layer has no idea what HTTP is. That's by design.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 3: The Repository
&lt;/h3&gt;

&lt;p&gt;The repository owns data access. SQL queries, ORM calls, database-specific logic it all lives here and only here.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// repositories/customerRepository.js&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SELECT * FROM customers WHERE id = ?&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One question drives this layer: How do we retrieve or store data?&lt;/p&gt;

&lt;p&gt;Not: &lt;em&gt;Should this order be approved? That answer belongs two layers up.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What You Actually Gain
&lt;/h2&gt;

&lt;p&gt;Loose coupling means layers depend on contracts, not implementations. The call chain looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Controller
    ↓
Service
    ↓
Repository
    ↓
Database
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each layer is independently replaceable. Here's what that buys you in practice:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Testing&lt;/strong&gt;. You can test business logic without a database. You can test HTTP behavior without mocking business rules. Tests become fast and targeted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Refactoring&lt;/strong&gt;. Migrating from MySQL to PostgreSQL means touching one layer the repository. Business logic is untouched. Nothing breaks accidentally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Onboarding&lt;/strong&gt;. A new engineer reads the service layer to understand what the business does. They read the repository to understand data access. No layer bleeds into another.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI-assisted development&lt;/strong&gt;. This one is underrated. When you ask an AI to regenerate a repository, it can do so without touching business logic. When you update an endpoint, you don't rewrite database code. Defined layers make AI tools significantly more precise and less dangerous.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why AI Defaults to the Mess
&lt;/h2&gt;

&lt;p&gt;AI coding assistants are trained on tutorials, quick-start guides, and Stack Overflow answers. That code is written to demonstrate a concept quickly not to model production architecture.&lt;/p&gt;

&lt;p&gt;These tools optimize for the shortest path to a visible result. The output looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Route
 ├─ Validation
 ├─ Business Rules
 ├─ SQL Queries
 ├─ External API Calls
 └─ Response Formatting
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Day one feels productive. You're shipping. It runs.&lt;/p&gt;

&lt;p&gt;Month six: every feature touches every file. Bug fixes create side effects. Nobody wants to refactor because nobody knows what else will break.&lt;/p&gt;

&lt;p&gt;The velocity you gained upfront was borrowed against your future team's sanity.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to Use AI Without the Mess
&lt;/h2&gt;

&lt;p&gt;Define the architecture first. Then ask AI to fill in the layers.&lt;/p&gt;

&lt;p&gt;Be explicit with your prompts:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Generate the service layer only. Assume a repository interface exists. No database queries. No HTTP handling."&lt;/p&gt;

&lt;p&gt;"Write a repository for the orders table. Return raw data objects. No business logic."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;AI tools are excellent at implementing patterns when the boundaries are clear. The boundaries however are your job to set. That's not changing anytime soon.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;AI generates code. Architecture determines whether that code survives real usage.&lt;/p&gt;

&lt;p&gt;Layered architecture isn't a large-team luxury. It's the structure that lets AI-generated applications grow without becoming a liability.&lt;/p&gt;

&lt;p&gt;The faster we build, the more separation of concerns matters. Speed without structure is just debt with better marketing.&lt;/p&gt;

&lt;p&gt;Define your layers. Enforce your boundaries. Then let the AI fill them in.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;What's your approach to structuring AI-generated code? Drop it in the comments curious what's working across different stacks and team sizes.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;If this hit close to home I write about exactly this stuff (what AI gets wrong in software architecture, and what good systems actually look like) in a newsletter called &lt;strong&gt;AI + Architecture&lt;/strong&gt;. No fluff, just real breakdowns from actual builds.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://newsletter.aidevbuilds.com/subscribe" rel="noopener noreferrer"&gt;Subscribe here →&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>node</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
