<?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: Andrew Boyko</title>
    <description>The latest articles on DEV Community by Andrew Boyko (@andriiboyko).</description>
    <link>https://dev.to/andriiboyko</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%2F3148430%2F126f7bdb-5753-4dde-9bb7-8658c467fa67.png</url>
      <title>DEV Community: Andrew Boyko</title>
      <link>https://dev.to/andriiboyko</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/andriiboyko"/>
    <language>en</language>
    <item>
      <title>CQRS in NestJS: When It's Worth the Complexity</title>
      <dc:creator>Andrew Boyko</dc:creator>
      <pubDate>Thu, 02 Jul 2026 14:16:38 +0000</pubDate>
      <link>https://dev.to/andriiboyko/cqrs-in-nestjs-when-its-worth-the-complexity-2f5n</link>
      <guid>https://dev.to/andriiboyko/cqrs-in-nestjs-when-its-worth-the-complexity-2f5n</guid>
      <description>&lt;p&gt;The person most responsible for the industry-standard explanation of CQRS is also the person telling you not to use it. Martin Fowler's bliki entry on CQRS, the page half of the backend world links to when they explain the pattern, says plainly: "you should be very cautious about using CQRS." Not a hedge. Not a "consider the tradeoffs." A direct warning, from the guy whose writeup made the term mainstream.&lt;/p&gt;

&lt;p&gt;That's not a contradiction. It's the actual state of the pattern: genuinely useful in a narrow set of cases, and a productivity tax everywhere else. The hard part isn't understanding CQRS. It's telling which side of that line your service is on.&lt;/p&gt;

&lt;h2&gt;
  
  
  What CQRS Separates
&lt;/h2&gt;

&lt;p&gt;CQRS traces back further than most people assume. Bertrand Meyer's Command-Query Separation principle, from his work on Eiffel, said a method should either change state or return data, never both, at the level of a single function. CQRS takes that idea and moves it up a layer: instead of one function doing one thing, it's your write model and your read model that split. Udi Dahan was applying this to service-oriented systems as early as 2008, formalizing it as "Clarified CQRS" in 2009. Greg Young popularized the term and the modern shape of the pattern around 2010, usually alongside domain-driven design.&lt;/p&gt;

&lt;p&gt;The core move: commands change state and return nothing meaningful. Queries return data and change nothing. Once you accept that split, you're free to give commands and queries completely different models, different validation rules, even different storage, because nothing has to reconcile a single "the truth" schema that serves both jobs at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Read/Write Skew That Justifies It
&lt;/h2&gt;

&lt;p&gt;Fowler names two legitimate reasons to reach for CQRS: a domain complex enough that a unified model would need to compromise for both directions, or a real performance requirement to scale reads and writes independently. Everything else is optional complexity dressed up as architecture.&lt;/p&gt;

&lt;p&gt;The skew that matters isn't "we have more reads than writes." Almost every system does. It's a &lt;em&gt;structural&lt;/em&gt; mismatch: your write side needs rich validation, business rules, and multi-step invariants, while your read side needs to answer questions your write model was never shaped to answer efficiently. A booking system that validates availability, conflicts, and cancellation policy on write, but needs to answer "show me every booking across three time zones grouped by room" on read, has structural skew. A blog with a &lt;code&gt;posts&lt;/code&gt; table and a &lt;code&gt;title&lt;/code&gt;/&lt;code&gt;body&lt;/code&gt;/&lt;code&gt;published_at&lt;/code&gt; doesn't, no matter how many more reads than writes it serves.&lt;/p&gt;

&lt;p&gt;If your honest answer to "why can't the same model serve both" is "it could, we just don't want to write two queries," that's not skew. That's an excuse.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the NestJS CQRS Module Gives You
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;@nestjs/cqrs&lt;/code&gt; gives you three buses: &lt;code&gt;CommandBus&lt;/code&gt;, &lt;code&gt;QueryBus&lt;/code&gt;, and &lt;code&gt;EventBus&lt;/code&gt;. All three are built on RxJS Observables, so you can subscribe to the whole stream of commands, queries, or events flowing through your application, not just the individual handler results.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;src/scheduling/commands/create-swap-request.handler.ts&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;CommandHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CreateSwapRequestCommand&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreateSwapRequestHandler&lt;/span&gt;
  &lt;span class="k"&gt;implements&lt;/span&gt; &lt;span class="nx"&gt;ICommandHandler&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;CreateSwapRequestCommand&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="na"&gt;shifts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ShiftRepository&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CreateSwapRequestCommand&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&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;shift&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shifts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shiftId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;shift&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requestSwap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;requestedBy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shifts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;shift&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;shift&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&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;The command handler owns validation and business rules. It doesn't return the shift, a DTO, or anything the frontend would render, just enough to confirm the write happened. A matching query handler pulls from whatever shape is fastest to read, which does not have to be the same tables the command touched.&lt;/p&gt;

&lt;p&gt;One detail that catches people off guard: &lt;code&gt;CommandBus&lt;/code&gt;, &lt;code&gt;QueryBus&lt;/code&gt;, and &lt;code&gt;EventBus&lt;/code&gt; are singletons, so combining them with NestJS's request-scoped providers takes extra care. The library handles this by spinning up a new instance of a request-scoped handler for each command, query, or event it processes, rather than sharing one instance across requests. It works, but it's not obvious from the decorator syntax that anything special is happening underneath.&lt;/p&gt;

&lt;p&gt;Event handlers run asynchronously and are expected to handle their own exceptions. An event handler that throws doesn't crash the request that published the event, it gets caught, wrapped into an &lt;code&gt;UnhandledExceptionInfo&lt;/code&gt;, and pushed onto a separate &lt;code&gt;UnhandledExceptionBus&lt;/code&gt; stream.&lt;/p&gt;

&lt;p&gt;If nothing is listening to that stream, the failure disappears silently. First time I saw this, I assumed a failing event handler would at least log something by default. It doesn't. You have to wire that up yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  CQRS Is Not Event Sourcing
&lt;/h2&gt;

&lt;p&gt;These two get bundled together constantly, partly because they show up in the same tutorials and partly because event sourcing naturally produces a write side and a read side that already look like CQRS. But they're independent decisions.&lt;/p&gt;

&lt;p&gt;Event sourcing means your source of truth is an append-only log of events, and current state is a projection computed by replaying them. CQRS means your read and write models are separate. You can event-source without CQRS (replay events into the same model you write through). You can do CQRS without event sourcing (a normal relational write model, plus a denormalized read table kept in sync by a projector). NestJS's CQRS module happens to ship with &lt;code&gt;AggregateRoot&lt;/code&gt; and event-emitting building blocks that make it easy to slide into event sourcing, but nothing about &lt;code&gt;CommandBus&lt;/code&gt; and &lt;code&gt;QueryBus&lt;/code&gt; requires it.&lt;/p&gt;

&lt;p&gt;Conflating the two is how projects end up with the operational cost of an event store (replay logic, event versioning, snapshotting) when all they actually needed was a read-optimized table.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Failure Mode: Applying CQRS to Everything
&lt;/h2&gt;

&lt;p&gt;The most common way teams burn the investment is treating CQRS as a system-wide default instead of a targeted tool. Fowler's own framing: many systems fit a CRUD mental model just fine, and forcing CQRS onto them is "a significant mental leap for all concerned" with no matching payoff.&lt;/p&gt;

&lt;p&gt;The failure compounds in a microservices context. If you don't identify your aggregate boundaries correctly first, and then apply CQRS on top of that mistake, you end up splitting a single aggregate's data across services, because the read side "needs" a view that spans what should have been one consistency boundary. That's not CQRS creating the mess, it's CQRS making an existing modeling mistake more expensive to carry.&lt;/p&gt;

&lt;p&gt;The rule that scales: apply CQRS to the specific bounded contexts where the skew is real, never to a whole application by default. A platform can have three services where CQRS earns its keep and twelve where a repository and a DTO are all anyone needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Eventual Consistency Tax You're Paying
&lt;/h2&gt;

&lt;p&gt;Splitting the models means the read side can lag the write side. How much depends entirely on your projection mechanism, synchronous in the same transaction, asynchronous via a message queue, or somewhere in between, but the lag is never exactly zero once the models are meaningfully separate.&lt;/p&gt;

&lt;p&gt;The concrete version of this problem: a user places an order, and it can take anywhere from a couple of seconds to a couple of minutes before that order shows up in their order history, depending on how the projection pipeline is built. That's not a bug. It's the actual cost of the pattern, and it has to be a product decision, not something the write side discovers by accident when a support ticket comes in asking why a booking "disappeared."&lt;/p&gt;

&lt;p&gt;If your UI can't tolerate that lag anywhere in the flow, either the projection needs to be synchronous for that specific path, defeating some of the scaling benefit, or CQRS is the wrong tool for that particular read.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I'd Reach for This
&lt;/h2&gt;

&lt;p&gt;On the healthcare platform I work on now, the scheduling side has exactly this shape: writing a shift change means checking coverage rules, skill-level requirements, and conflict windows across a rotation, real domain complexity on the command side, while the read side mostly needs to answer "show me the week" in a dozen different groupings (by provider, by department, by shift type) fast enough for a calendar UI that gets hit constantly. That's read/write skew, not a preference.&lt;/p&gt;

&lt;p&gt;Credentialing, on the same platform, mostly isn't. A verification record has a status, an expiration date, and an audit trail. The queries against it are close enough to the write shape that splitting the models would add a synchronization problem without buying anything back. One service, two very different answers to "should this use CQRS," and the difference is the skew, not the domain's importance.&lt;/p&gt;

&lt;p&gt;That's the actual decision procedure: not "is this domain important" or "are we doing microservices," but "would the read model and the write model genuinely want to be different shapes, and is the eventual-consistency cost something the product can absorb." If both answers are yes, the complexity Fowler warns about is the complexity you're supposed to be paying for.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.andriiboyko.com/articles/cqrs-in-nestjs-when-its-worth-the-complexity" rel="noopener noreferrer"&gt;andriiboyko.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ddd</category>
      <category>node</category>
      <category>cqrs</category>
      <category>nestjs</category>
    </item>
    <item>
      <title>Bounded Contexts in a Real Codebase, Not a Diagram</title>
      <dc:creator>Andrew Boyko</dc:creator>
      <pubDate>Thu, 02 Jul 2026 14:16:24 +0000</pubDate>
      <link>https://dev.to/andriiboyko/bounded-contexts-in-a-real-codebase-not-a-diagram-4cp4</link>
      <guid>https://dev.to/andriiboyko/bounded-contexts-in-a-real-codebase-not-a-diagram-4cp4</guid>
      <description>&lt;p&gt;Everyone draws the same diagram. A box for each part of the domain, a straight line from each box to a service, one arrow per box. It's the tidiest picture you'll ever ship in an architecture review, and the moment you try to build it exactly as drawn, it starts to lie to you.&lt;/p&gt;

&lt;p&gt;I've now been on both sides of that diagram: the person who drew it with total confidence in year one, and the person who had to explain eighteen months later why the "Credentialing" box didn't match the credentialing service. The mismatch wasn't a mistake. It's what happens when you take a modeling concept and treat it like a deployment topology.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a Bounded Context Is
&lt;/h2&gt;

&lt;p&gt;The term comes from Eric Evans's 2003 book &lt;em&gt;Domain-Driven Design: Tackling Complexity in the Heart of Software&lt;/em&gt;. A bounded context is the boundary inside which a particular model, and the language that describes it, stays consistent. Outside that boundary, the same word can mean something else entirely, and that's fine, because you're no longer inside the model where it was defined.&lt;/p&gt;

&lt;p&gt;The canonical example: "Customer" in a sales context means someone who can place an order. "Customer" in a support context means someone with a ticket history and an SLA. Same word, two different models, two different sets of rules about what's valid. A bounded context is where you stop pretending those are the same thing and give each one its own vocabulary, its own invariants, and its own boundary.&lt;/p&gt;

&lt;p&gt;That's a modeling concept. It says nothing about how many services you run, how you deploy them, or who's on call for which one. Which is exactly where the diagram goes wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  The One-to-One Assumption, and Why It Breaks
&lt;/h2&gt;

&lt;p&gt;The popular shorthand is: one bounded context, one microservice. It's easy to teach, easy to draw, and easy to sell in a slide deck. DDD writer Vlad Khononov put the actual relationship more precisely: "A Microservice is a Bounded Context, but not vice versa. Not every Bounded Context is a Microservice." A bounded context defines the largest boundary inside which a model can stay consistent. A microservice is a deployment decision made inside that boundary, and sometimes below it, not a mirror of it.&lt;/p&gt;

&lt;p&gt;Here's the failure mode in practice. A team draws contexts too small, one per entity, and ends up with a dozen services that all have to call each other synchronously to do anything useful. That's not microservices. That's a monolith with the process boundaries moved into the network, which is strictly worse: same coupling, new latency, new failure modes.&lt;/p&gt;

&lt;p&gt;The industry has a name for it now: the distributed monolith. It shows up most often when a team splits a system along its existing code layers (controllers, services, repositories) instead of along real domain boundaries, then calls the result "microservices" because there are multiple deployables.&lt;/p&gt;

&lt;p&gt;The opposite mistake is just as common and less talked about: drawing one context for the entire domain, then building a single service with DDD vocabulary sprinkled over what is, underneath, still a big ball of mud. You get the ceremony of bounded contexts (aggregates, repositories, a ubiquitous language doc nobody updates) without the actual isolation that makes any of it worth doing.&lt;/p&gt;

&lt;p&gt;The honest answer is that a bounded context and a service are two different kinds of boundary, and they only sometimes line up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One bounded context can span multiple services, when the context is large enough that splitting it makes operational sense (different scaling needs, different release cadence, different team).&lt;/li&gt;
&lt;li&gt;Multiple small, tightly related contexts can live inside one service, when splitting them would only add network calls without adding independence.&lt;/li&gt;
&lt;li&gt;A context can be split across a synchronous API and an asynchronous event stream, which is not "two contexts," it's one context with two integration surfaces.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Deciding which of these is true for a given part of your domain depends on things a bounded context diagram can't tell you: team size, deployment cadence, scaling profile, and how often the boundary needs to move.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where a Real Split Came From
&lt;/h2&gt;

&lt;p&gt;I've spent the last year leading the technical direction of a healthcare workforce platform built around two products: a staff scheduling system and a credentialing system. On paper, that's two bounded contexts and, if you followed the popular rule, two services. In practice, the platform runs scheduling, credentialing, and integrations as separate services, and that third one is the interesting part.&lt;/p&gt;

&lt;p&gt;Integrations wasn't its own bounded context in the domain sense. It doesn't own a piece of business vocabulary the way scheduling ("shift," "coverage," "swap request") or credentialing ("primary source verification," "expiration," "attestation") do. What it owns is a boundary problem: every external system we talk to (identity providers, document verification vendors, notification services) has its own model, its own failure modes, and its own pace of change, and none of that should leak into the scheduling or credentialing models.&lt;/p&gt;

&lt;p&gt;That's the anti-corruption layer pattern from context mapping, given its own service instead of a library everyone imports. Once external integrations lived behind their own boundary, the scheduling and credentialing services stopped needing to know anything about a specific vendor's API shape, retry semantics, or auth flow. They call an internal contract; the integrations service is the only thing that has to change when a vendor changes theirs.&lt;/p&gt;

&lt;p&gt;That's a service boundary that exists for operational reasons (isolate volatility, isolate failure, let one team own vendor relationships) layered on top of a modeling boundary that exists for domain reasons (keep "shift" and "verification" from ever meaning two things at once). Stabilizing the platform's architecture meant getting that distinction right, not drawing a cleaner box diagram.&lt;/p&gt;

&lt;h2&gt;
  
  
  When One Service Should Hold Multiple Contexts
&lt;/h2&gt;

&lt;p&gt;The reverse case matters just as much, and it's the one people are more reluctant to admit out loud: sometimes the right call is to keep two related contexts in the same service, at least for a while.&lt;/p&gt;

&lt;p&gt;Two sub-domains that change together, get deployed together, and are owned by the same two-person team don't need a network boundary between them. Splitting them early buys you nothing except a second CI pipeline, a second on-call rotation to staff, and a synchronous call where a function call used to be. The tradeoff to weigh isn't "is this technically a separate context," it's "does separating this reduce coordination cost, or just add infrastructure?"&lt;/p&gt;

&lt;p&gt;Signals that it's still too early to split:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The two contexts are always deployed in the same release.&lt;/li&gt;
&lt;li&gt;One team owns both, and that's not changing soon.&lt;/li&gt;
&lt;li&gt;The only reason for the split is "microservices are the right architecture," not a concrete scaling or ownership problem.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Signals that it's worth the network hop:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The contexts have genuinely different scaling profiles (one is read-heavy and cacheable, the other is write-heavy and transactional).&lt;/li&gt;
&lt;li&gt;Different teams need to own them, and shared deployment is actively slowing both down.&lt;/li&gt;
&lt;li&gt;One context's failure shouldn't be able to take the other down, and today it can.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are visible on a static bounded-context diagram. They're operational facts about the system you're running right now, and they change over time, which is why context boundaries and service boundaries are allowed to diverge and then re-converge later.&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%2Fwww.andriiboyko.com%2Fassets%2Fimgs%2Farticles%2Fbounded-contexts-in-a-real-codebase-not-a-diagram%2Fcontext-map-scheduling-credentialing-integrations.svg" 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%2Fwww.andriiboyko.com%2Fassets%2Fimgs%2Farticles%2Fbounded-contexts-in-a-real-codebase-not-a-diagram%2Fcontext-map-scheduling-credentialing-integrations.svg" alt="Diagram showing Scheduling and Credentialing services connecting through an internal contract to an Integrations anti-corruption layer, which connects via vendor-specific contracts to an identity provider, verification vendor, and notification service" width="1600" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Seams: Context Mapping Patterns Worth Knowing
&lt;/h2&gt;

&lt;p&gt;Once you accept that contexts and services don't automatically line up, the interesting question becomes: what sits at the seam? DDD's context mapping patterns answer that, and three of them cover most real cases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shared kernel&lt;/strong&gt; is when two contexts agree to share a small, explicitly maintained subset of the model, usually because splitting it would cost more than the coordination overhead of keeping it in sync. It's the pattern to reach for rarely and deliberately, not the default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Customer/supplier&lt;/strong&gt; is when one context's team can influence the other's roadmap because they depend on it, and the supplier team treats that dependency as a real constraint on what they ship. This is a people pattern as much as a code pattern: it only works if the supplier team honors it in practice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Anti-corruption layer&lt;/strong&gt; is the one that shows up the most in practice, and it's the pattern the integrations service above is built on. A translation boundary sits between two models so that neither one has to bend to accommodate the other. A small TypeScript sketch of the shape:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;src/integrations/verification/anti-corruption-layer.ts&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The vendor's shape. We don't control this and it can change without warning.&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;VendorVerificationResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;verification_status_code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;V1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;V2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;V3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;subject_ref&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;checked_at_utc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// Our domain's shape. This is the only thing Credentialing ever sees.&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;PrimarySourceVerification&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;verified&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;providerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;verifiedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;translate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;vendorResponse&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;VendorVerificationResponse&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;PrimarySourceVerification&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;statusMap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;VendorVerificationResponse&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;verification_status_code&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;PrimarySourceVerification&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;status&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;V1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;V2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;verified&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;V3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failed&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;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;statusMap&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;vendorResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verification_status_code&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;providerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;vendorResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subject_ref&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;verifiedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;vendorResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verification_status_code&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;V2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;vendorResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;checked_at_utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&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;Nothing clever is happening here on purpose. That's the point: the translation function is the entire cost of keeping the vendor's model out of the domain. When the vendor renames a field or adds a fourth status code, this function is the only place that changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding the Boundaries Before You Build Anything
&lt;/h2&gt;

&lt;p&gt;If you're doing this for the first time, don't start by drawing boxes. Start with event storming: get the people who understand the domain in a room (or a shared board), and map out the significant business events on a timeline, in the language the business uses for them, not the language your database schema uses. Boundaries tend to reveal themselves as clusters: events that trigger each other tightly belong together, and gaps where one cluster hands off to another, often with a change in vocabulary, are your candidate context boundaries.&lt;/p&gt;

&lt;p&gt;It's slower than opening a whiteboard and drawing four boxes from memory. It's also the difference between a boundary that reflects how the business actually works and a boundary that reflects how the code happened to be organized when you looked at it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the Boundary Should Have Been
&lt;/h2&gt;

&lt;p&gt;If I were drawing that diagram again for the platform I'm working on now, I wouldn't add a fourth box for "Integrations" as if it were a peer domain concept next to scheduling and credentialing. I'd draw it as what it is: an anti-corruption layer that earned its own deployable because of operational concerns, not because the domain model demanded a third bounded context. The distinction matters, because the next time someone asks "should this be its own service," the domain diagram won't answer the question. The operational tradeoffs will.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.andriiboyko.com/articles/bounded-contexts-in-a-real-codebase-not-a-diagram" rel="noopener noreferrer"&gt;andriiboyko.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>ddd</category>
      <category>bounded</category>
      <category>contexts</category>
    </item>
  </channel>
</rss>
