gsantana.dev

Bounded Contexts

One model to rule them all sounds great until "Customer" means five different things. Draw the boundaries before the boundaries draw you.

31 min read

Introduction

Every system starts small and honest. There's an Order table, a Customer table, a few services talking to each other, and everyone in the room means the same thing when they say "order". Then the company grows, new teams show up, sales wants one thing, billing wants another, logistics wants a third, and that innocent Customer table ends up with 80 columns, half of them nullable, and a comment at the top that says "don't touch, ask Carlos".

Bounded Contexts are the answer Domain-Driven Design (DDD) gives to that mess. The goal of this principle is simple to say and hard to practice: split a large domain into smaller models, each with a clear boundary, its own language and its own owner, and make the relationships between those models explicit instead of accidental.

When a team ignores this principle, the symptoms show up sooner than anyone expects:

  • The same word means different things in different meetings, and nobody notices until a bug reaches production;
  • A single "canonical" model tries to serve every department and ends up serving none of them well;
  • Every change in one area breaks something in an area nobody on the team has ever heard of;
  • Teams wait on each other for every release, because everything touches everything;
  • Microservices that share one database and must be deployed together (the famous distributed monolith);
  • Integrations with legacy systems leak their weird names and rules into brand new code;

Yep, it's rare, but it happens all the time... Who hasn't opened a class called CustomerHelperManagerService and felt a small piece of their soul leave their body?

Bounded Contexts are not a technology, a framework or a deployment unit. They are a modeling decision: where one model ends and another begins. Services, databases and teams can (and often should) follow those lines, but the boundary comes first.

Ubiquitous Language: the word is the design

Before we talk about boundaries, we need to talk about language. Eric Evans, who wrote the original DDD book, called it the Ubiquitous Language: a shared, rigorous vocabulary used by domain experts and developers alike, in conversations, in documents, in tests and, above all, in the code.

If the business says "the policy is reinstated" and the code says setStatus(3), you have a translation layer living inside people's heads. Every translation is a chance for a misunderstanding, and misunderstandings compile just fine.

A healthy ubiquitous language has a few properties:

  • It lives in the code. Class names, method names, events and API fields use the business terms, not technical approximations;
  • It is precise. "Active customer" has a definition that everyone can repeat, not a feeling;
  • It evolves. When a conversation with a domain expert reveals a better term, the code gets renamed. Refactoring the language is refactoring the design;
  • It has a boundary. And this is the key part: a ubiquitous language is only ubiquitous inside one context.

The "Customer means five different things" problem

Ask five departments what a "customer" is and you'll get five honest, correct and incompatible answers.

One word, five models The word Customer sits in the center, connected to five contexts: Sales sees a lead in the pipeline, Billing sees whoever pays the invoice, Shipping sees a name and an address, Support sees who opened the ticket, and Marketing sees a segment member. ONE WORD, FIVE MODELS Customer one table, 80 columns Sales a lead in the pipeline Billing whoever pays the invoice Shipping a name and an address Support who opened the ticket Marketing a segment member
Figure 1: The same word, five legitimate meanings
  • For Sales, a customer is a lead with a stage in the funnel, an owner and a probability of closing;
  • For Billing, a customer is whoever is legally responsible for paying: a tax ID, a payment method, a billing address, a credit limit;
  • For Shipping, a "customer" barely exists. What matters is a recipient: a name, an address, a delivery window;
  • For Support, a customer is whoever opened the ticket, with a plan, an SLA and a history of complaints;
  • For Marketing, a customer is a member of a segment, with consent flags and campaign history.

The classic mistake is to look at this and say "great, let's build one Customer entity that covers all of it". That's how you get the 80-column table, the isLead flag, the shippingAddress2Old field and the meeting where three teams argue about whether status = 'ACTIVE' means "paid this month" or "logged in this month".

Confused junior dev
Naive Junior
"But isn't that duplication? DRY, right? If we have five Customer classes, we're repeating ourselves five times!"

Easy there, Junior! DRY is about not duplicating knowledge, not about never having two classes with the same name. The shipping recipient and the billing payer are different concepts that happen to share a word. Forcing them into one class doesn't remove duplication, it creates coupling: now every change to billing rules has to be negotiated with shipping, and vice versa.

The DDD answer is: each context gets its own model of "customer", shaped exactly for its job, and the contexts share only an identifier (a customerId) and whatever facts they explicitly agree to exchange. Five small, sharp models beat one big, blurry one.

Subdomains: where to spend your best people

A domain is the area of business your software serves. Large domains are made of subdomains, and not all of them are equally important. DDD classifies them into three types, and this classification should drive where you invest.

TypeWhat it isHow to treat it
CoreWhat makes the business different from competitors. The reason customers choose you.Build it in-house, with your best people, rich domain models and constant refinement. This is where DDD pays off the most.
SupportingNecessary and somewhat specific to your business, but not a differentiator.Build it, but keep it simple. CRUD is often fine. Consider outsourcing.
GenericProblems every company has and that are already solved: identity, email, payments, accounting.Buy it or use a SaaS or open source solution. Don't reinvent authentication.

For an e-commerce company that competes on dynamic pricing, Pricing is core. Order management might be core too. Shipping is supporting (it needs to work well, but it's not why people buy from you). Identity and payments are generic: use a proven provider and move on.

A quick smell test: if a competitor copied this subdomain tomorrow, would it hurt? If the answer is "not really", it's not core. Teams love to treat everything as core, because core is where the interesting problems live. Resist that. Spending your best engineers on a homemade login system is a strategic mistake, not a technical one.

Subdomains belong to the problem space (how the business is organized). Bounded contexts belong to the solution space (how we model and build it). Ideally they line up one to one, but in real life a legacy system may cover three subdomains, or a single subdomain may need two contexts. Knowing the difference helps you talk about the gap.

Bounded Contexts: drawing the lines

A bounded context is an explicit boundary within which a particular model applies and a particular ubiquitous language is consistent. Inside the boundary, "Order" means exactly one thing. Outside, it may mean something else, and that's fine.

In practice, a bounded context usually comes with:

  • Its own model: entities, value objects, aggregates and rules, designed for that context only;
  • Its own language: documented terms, ideally in a small glossary that lives in the repository;
  • Its own data: a database or at least a schema that only this context writes to;
  • Its own team: one team owns it end to end. A team may own several contexts, but a context shouldn't have several owning teams;
  • An explicit interface: an API, events, or a published contract. Nobody reaches into its internals.

Finding the boundaries

There's no algorithm that spits out perfect contexts, but there are good signals:

1. Language changes

When the same word starts meaning something different, or when domain experts start using a different vocabulary, you're probably crossing a boundary. Listen for "well, for us an order is only an order after it's paid".

2. Different rates of change

Pricing rules change every week; the tax engine changes once a year when the law changes. Parts that change at different speeds and for different reasons want to live apart.

3. Different experts

If the people you need to talk to in order to understand an area are different people, that's a strong hint. The finance team doesn't care how the warehouse picks items, and the warehouse doesn't care about revenue recognition.

4. Consistency needs

Things that must be consistent in the same transaction usually belong together. Things that can tolerate a few seconds (or minutes) of delay can be split and integrated with events.

5. Business capabilities

Capabilities such as "take orders", "bill customers", "ship packages" are more stable than org charts and much more stable than technology. They are a good first cut.

Goal: each context should be small enough to be understood by one team and cohesive enough that most changes stay inside it.

Benefit: teams can change their model freely without asking permission from the whole company, as long as they honor their published contracts.

Event Storming: discovering boundaries together

Drawing contexts alone in a meeting room is a recipe for a beautiful diagram that nobody agrees with. Event Storming, created by Alberto Brandolini, is a workshop format that puts developers and domain experts in front of a long wall (or a virtual board) to map the business as a sequence of domain events.

The basic flow looks like this:

  1. Chaotic exploration: everyone writes domain events on orange sticky notes, in the past tense: "Order Placed", "Payment Authorized", "Package Shipped", "Refund Requested". No discussion yet, just volume;
  2. Enforce the timeline: put events in chronological order. Duplicates and contradictions appear, and that's the point;
  3. Hot spots: mark areas of confusion, disagreement or pain with bright pink notes. These are gold: they show where the language is broken;
  4. Commands and actors: add what triggers each event (blue for commands, small yellow for actors or roles);
  5. Policies and external systems: "whenever X happens, do Y" rules and the systems you depend on;
  6. Find the boundaries: look for clusters of events that use the same language and are owned by the same people. Draw lines around them. Those are your candidate bounded contexts.

The magic of Event Storming is not the sticky notes. It's watching someone from sales and someone from finance argue for ten minutes about what "Order Confirmed" means and realizing that you just found a context boundary for free.

Confused junior dev
Naive Junior
"So... each bounded context is a microservice, right? We run the workshop, draw eight circles and tomorrow we create eight repositories and eight databases!"

Hold on, Junior! That's exactly the mistake that creates the most expensive systems in the industry. We'll get there in a moment, it deserves its own section.

Context Mapping: the relationships matter as much as the boxes

Contexts don't live alone. Orders need prices, billing needs orders, shipping needs addresses. A context map makes those relationships explicit: who depends on whom, who has the power to change the contract, and how models are translated at the border.

These are the classic patterns:

PatternWhat it meansWhen to use it
PartnershipTwo teams succeed or fail together and coordinate changes closely.Two core contexts that evolve together, with teams that talk every day.
Shared KernelTwo contexts share a small, explicitly defined piece of model or code.A tiny, stable subset (like a Money or Address value object). Keep it minimal, changes need both teams' approval.
Customer/SupplierThe upstream (supplier) serves the downstream (customer), and the downstream's needs influence the upstream's roadmap.Most internal integrations. The downstream team has a voice in planning.
ConformistThe downstream simply adopts the upstream's model, with no translation.The upstream won't change for you (a big vendor, a powerful team) and its model is good enough.
Anti-Corruption Layer (ACL)The downstream builds a translation layer to protect its model from the upstream's.Legacy systems, external APIs with poor models, anything you don't want leaking into your core.
Open Host Service (OHS)The upstream exposes a well-defined protocol for many consumers.A context used by many others, like identity or catalog.
Published Language (PL)A documented, shared exchange format (often paired with OHS).Public events, industry standards, versioned schemas.
Separate WaysNo integration at all. Each context solves its own problem.When integrating costs more than the benefit. Sometimes duplicating a small feature is the right call.

Here's what a context map for our e-commerce example could look like:

Context map of an e-commerce system Identity is a generic context exposed as an open host service with a published language to Pricing, Ordering and Billing. Pricing and Ordering are core contexts in a partnership. Ordering supplies Billing as customer and supplier and shares a kernel with Shipping. Shipping conforms to an external carrier API. Billing reads a legacy ERP through an anti-corruption layer. Marketing goes separate ways. CONTEXT MAP Marketing separate ways Identity generic Core Supporting Generic External Legacy OHS / PL Pricing core Ordering core Billing supporting PARTNERSHIP CUSTOMER / SUPPLIER SHARED KERNEL Carrier API external Shipping supporting CONFORMIST Legacy ERP legacy ACL
Figure 2: A context map shows the boxes and, more importantly, the power dynamics between them

Notice that the context map is not only technical. It's political. "Conformist" is an honest admission that you have no leverage over the upstream. "Customer/Supplier" only works if the upstream team actually listens. "Partnership" requires two teams with aligned goals and real communication. Drawing the map forces those conversations to happen out loud, instead of being discovered during an incident.

Be careful with Shared Kernel. It starts as "just the Address class" and, six months later, it's a shared library with 40 classes that three teams have to coordinate releases around. If it grows, it's no longer a kernel, it's a monolith in disguise. Keep it tiny, versioned and boring.

Anti-Corruption Layer: keeping the legacy out

Let's zoom in on the most useful pattern for anyone who has ever integrated with a legacy system: the Anti-Corruption Layer.

Picture the scenario. Your new Billing context has a clean model: Invoice, Payer, Money, InvoiceStatus. But the company's 20-year-old ERP is still the source of truth for customer credit data, and its API returns things like this:

{
  "CD_CLI": "000482",
  "NM_RAZ": "ACME LTDA",
  "TP_DOC": 3,
  "VL_LIM_CRED": "15000,00",
  "FL_BLOQ": "S"
}

TP_DOC = 3 means "company" (unless it's a branch, then it's 4, except in records created before 2011). FL_BLOQ = "S" means blocked, and the amount is a string with a comma as decimal separator. If you let this shape into your domain model, your shiny new context will be speaking ERP within a month.

The ACL is a layer that belongs to your context and whose only job is translation. It usually has three parts:

  • Facade: a simplified interface over the legacy system, exposing only what you need;
  • Adapter: handles the technical details: protocol, authentication, retries, pagination, weird encodings;
  • Translator: converts the legacy model into your domain model (and back, if needed), including all the business quirks.
Anti-corruption layer between Billing and a legacy ERP The Billing context, with its own model of Invoice, Payer and Money, talks to an anti-corruption layer made of a facade, a translator and an adapter. The layer talks to the legacy ERP and its cryptic fields, so the legacy model never reaches Billing. OUR MODEL Billing Invoice Payer Money OWNED BY BILLING Anti-Corruption Layer Facade Translator Adapter THEIR MODEL Legacy ERP CD_CLI, NM_RAZ TP_DOC = 3 FL_BLOQ = "S" translation happens in one place; the legacy model never leaks in
Figure 3: The ACL belongs to the downstream context and absorbs all the legacy weirdness

In code, the translator is often surprisingly small and boring, which is exactly what you want:

// Billing's own model: no trace of the ERP here
type PayerKind = "individual" | "company";
 
interface Payer {
  id: PayerId;
  legalName: string;
  kind: PayerKind;
  creditLimit: Money;
  blocked: boolean;
}
 
// ACL translator: the only place that knows what TP_DOC means
function toPayer(raw: ErpCustomerDto): Payer {
  return {
    id: PayerId.fromLegacy(raw.CD_CLI),
    legalName: raw.NM_RAZ.trim(),
    kind: raw.TP_DOC === 3 || raw.TP_DOC === 4 ? "company" : "individual",
    creditLimit: Money.brl(parseLegacyDecimal(raw.VL_LIM_CRED)),
    blocked: raw.FL_BLOQ === "S",
  };
}

The Billing domain works with Payer and never sees FL_BLOQ. When the ERP is finally replaced (it will be, someday, maybe), you rewrite the ACL, and the rest of Billing doesn't even notice. The ACL is also the natural place for the Strangler Fig strategy: route calls through it and move capabilities out of the legacy system one piece at a time.

Benefit: your domain model stays clean, legacy changes have a small blast radius, and the translation rules are tested in isolation instead of scattered across the codebase.

Bounded Contexts vs Microservices

Now back to Junior's question. It's the most common misconception in the whole topic, so let's be very clear:

A bounded context is a logical boundary. A microservice is a deployment boundary. A good microservice should not cross a context boundary, but a context does not need to be a microservice. One context can be a module inside a monolith, one service, or even several services.

The distributed monolith story

I've seen this movie more than once. A company decides it's time to "go microservices". The team runs a quick workshop, splits the old monolith by entity (a customer-service, an order-service, a product-service, an inventory-service) and, to save time, they all keep pointing at the same database. After all, the data is already there.

A year later:

  • Placing an order calls customer-service, which calls product-service, which calls inventory-service, which calls pricing-service. If any of them is slow, checkout is slow. If any of them is down, checkout is down;
  • A column rename in the shared database requires a coordinated deploy of six services, scheduled for Saturday at 2 AM;
  • Every feature touches four repositories, four pipelines and four teams;
  • The cloud bill tripled, latency doubled, and debugging requires distributed tracing across a dozen hops;
  • Nobody can deploy anything alone, which was the entire point of the migration.

That's the distributed monolith: all the coupling of a monolith, plus all the operational cost of a distributed system. The worst of both worlds. The root cause was not microservices themselves, it was splitting along the wrong lines (entities and tables instead of business capabilities and language) and sharing the data underneath.

Distributed monolith versus modular monolith On the left, three services call each other synchronously in a chain and share one database, so they deploy and fail together. On the right, a single deployable holds three modules, each with its own schema, talking through in-process events and public APIs, ready to be split later along proven seams. DISTRIBUTED MONOLITH Orders Billing Shipping sync calls Shared DB one schema for all deploy together, fail together MODULAR MONOLITH one deployable Orders Billing Shipping schema schema schema events and public APIs between modules split later along proven seams
Figure 4: Same three names, very different coupling

The modular monolith: contexts without the network

For many teams, the best first step is a modular monolith: one deployable application, internally divided into modules that follow bounded context lines. Each module:

  • Has its own internal model and exposes only a small public API (an interface, a facade, published events);
  • Owns its own tables, ideally in its own schema, and other modules never query them directly;
  • Communicates with other modules through that public API or through in-process events;
  • Has its boundaries enforced by tooling: architecture tests (ArchUnit, NetArchTest, dependency-cruiser), separate projects or packages, lint rules.

You get most of the modeling benefits of bounded contexts (clear language, isolated change, explicit contracts) without paying for network calls, distributed transactions, service discovery and a dozen pipelines. And if one module later needs to scale independently or be owned by a separate team, the seam is already there. Extracting a well-isolated module into a service is a weekend project. Untangling a big ball of mud is a two-year program.

When a context should become a service

Split a context into its own deployable when there's a concrete reason, such as:

  1. Independent scaling: its load profile is very different from the rest (search, image processing, pricing at Black Friday);
  2. Independent release cadence: a team needs to ship several times a day without coordinating with anyone;
  3. Team autonomy: a separate team owns it and the shared deploy has become a bottleneck;
  4. Different technology needs: a different runtime, language or data store really makes a difference;
  5. Fault isolation: a failure there must not take down the rest (see Resilience Patterns).

If none of these apply, a module is probably enough. Distribution is a cost you pay for a benefit, not a badge.

Data Ownership per Context

A boundary that stops at the code and ignores the data is not a boundary. The rule is simple and non-negotiable: each context owns its data, and only that context writes to it. Other contexts get the data through the owner's API or through the events it publishes.

What does that look like in practice?

1. No shared tables

If two contexts write to the same table, they are one context (whether you admit it or not). Reading another context's tables directly is almost as bad: your code now depends on their internal schema, and they can't refactor without breaking you.

2. Share identifiers, not rows

Billing stores the orderId, not a foreign key into Ordering's database. It can ask Ordering for details or keep its own copy of the few facts it needs.

3. Local copies are fine

Shipping can keep its own projection of the delivery address, updated by an OrderPlaced or AddressChanged event. That's not a bug, it's a deliberate read model. Each copy has exactly the shape its context needs.

4. Events are part of the published language

Domain events that cross boundaries (OrderPlaced, PaymentCaptured) are contracts. Version them, document them and treat breaking changes with the same care as a public API change. Internal events can change freely; public ones cannot.

5. Use the Outbox pattern for reliable publishing

Writing to your database and publishing an event are two operations that can fail independently. The Transactional Outbox saves the event in the same transaction as the state change and a relay publishes it afterward, so you never lose an event or publish one for a change that was rolled back.

public async Task PlaceOrder(PlaceOrderCommand cmd)
{
    var order = Order.Place(cmd.CustomerId, cmd.Items, _clock);
 
    await using var tx = await _db.Database.BeginTransactionAsync();
    _db.Orders.Add(order);
    _db.Outbox.Add(OutboxMessage.From(new OrderPlaced(order.Id, order.Total)));
    await _db.SaveChangesAsync();
    await tx.CommitAsync();
    // A background relay reads the outbox and publishes to the broker
}
ApproachBenefit
One writer per piece of dataNo hidden coupling through the database; each team can change its schema freely.
Integration through APIs and eventsContracts are explicit, versioned and testable.
Local read modelsEach context gets exactly the data shape it needs, with no runtime dependency on others for reads.
Transactional outboxState changes and published events stay consistent, even with failures in between.

Conway's Law and Team Topologies

In 1967, Melvin Conway observed that organizations design systems that mirror their own communication structure. Half a century later, it's still undefeated. If three teams build a compiler, you get a three-pass compiler. If one team owns Billing and Shipping together and nobody owns Pricing, your architecture will reflect exactly that, no matter what the diagram on the wiki says.

This has a direct consequence for bounded contexts: boundaries in the software only hold if they match boundaries in the organization. A context owned by three teams will be pulled in three directions. Two contexts owned by one overloaded team will slowly merge.

The practical move is known as the Inverse Conway Maneuver: design the team structure you want so that the architecture you want emerges naturally. The book Team Topologies, by Matthew Skelton and Manuel Pais, gives a useful vocabulary for this:

  • Stream-aligned teams: own a flow of business value end to end, usually one or more bounded contexts. Most teams should be this type;
  • Platform teams: provide internal services (deploy, observability, data platform) that reduce the cognitive load of stream-aligned teams;
  • Enabling teams: help other teams adopt new skills or practices, then step back;
  • Complicated-subsystem teams: own a part that requires deep specialist knowledge (a pricing engine, a video codec, a risk model).

And three interaction modes that map nicely to context mapping: collaboration (close to Partnership), X-as-a-Service (close to Open Host Service and Customer/Supplier) and facilitating (enabling teams helping others).

A good heuristic from Team Topologies is cognitive load: a team should own only as many contexts as it can truly understand. If a team can't explain its own domain model without opening the code, it owns too much. Split the ownership before the model rots.

Tradeoffs

Bounded contexts bring clarity, autonomy and models that fit their purpose. But, like every architectural decision, they come with a price tag. Pretending they're free is how teams end up hating DDD.

Duplication of data and models

The same concept appears in several contexts with different shapes. That's intentional, but it means more code, more mapping and more places to update when a genuinely shared fact changes (like a customer's legal name). You trade some duplication for a lot of independence. Just make sure it's a conscious trade.

Integration complexity

Every relationship on the context map is an integration to design, build, test, monitor and version. Contract tests, schema registries, event versioning, ACLs: all of that is real work. With too many small contexts, the integration cost can exceed the benefit. Fewer, larger contexts are often better than many tiny ones, especially early on.

Eventual consistency

Once contexts communicate through events, the system is no longer consistent at every instant. The order is placed, but the invoice appears a few seconds later. The business must accept that (and usually does, once someone explains that the old system also ran a nightly batch). You'll need idempotent consumers, retries, compensation (sagas) and user interfaces that handle "processing" states gracefully.

Tradeoffs with Reliability

Splitting into services introduces network calls and more failure modes. Without timeouts, retries, circuit breakers and asynchronous communication, more boundaries mean more ways to fail. See Reliability and Resilience Patterns.

Tradeoffs with Performance Efficiency

Translation layers, serialization and hops across the network add latency. Queries that used to be a single SQL join now need composition across contexts or dedicated read models. See Performance Efficiency.

Tradeoffs with Cost Optimization

More deployables mean more infrastructure, more pipelines, more databases and more observability data. A modular monolith keeps most of the modeling benefits at a fraction of the cost. See Cost Optimization.

Tradeoffs with Operational Excellence

Each context needs ownership, runbooks, dashboards and on-call. Tracing a request across contexts requires correlation IDs and distributed tracing from day one. See Operational Excellence and Observability First.

Confused junior dev
Naive Junior
"Wait... so if I get the boundaries wrong, I'm stuck with them forever?"

Not forever, Junior, but moving a boundary gets more expensive the more you've invested in it. That's exactly why it pays to start with a modular monolith, keep contexts a bit larger at first, and adjust as you learn. Boundaries are hypotheses about the business. Treat them like any other design decision: explicit, documented (an ADR helps a lot) and open to revision. That's the heart of Evolutionary Design.

Conclusion

Bounded Contexts are one of the most powerful ideas in software architecture, precisely because they're not about technology. They're about accepting that a large business can't be described by a single model, that words mean different things in different places, and that clear boundaries (in language, code, data and teams) are what allow a system to grow without collapsing under its own weight.

Get the language right inside each context. Classify subdomains so you invest where it matters. Make relationships explicit with a context map. Protect your model from legacy with anti-corruption layers. Give each context ownership of its data. And remember that a context is a modeling boundary first: whether it becomes a module or a microservice is a separate, later decision driven by real needs.

Most importantly: bounded contexts don't eliminate complexity, they put it in the right place. You trade accidental coupling for deliberate integration, and a single blurry model for several sharp ones. That's a trade worth making, as long as you make it consciously.

Next Steps

  1. Build a glossary for your current system Pick the ten most important business terms and ask different teams to define them. Wherever definitions disagree, you've likely found a context boundary.

  2. Classify your subdomains List the main areas of the business and label them core, supporting or generic. Check whether your best people and biggest investments are going to the core.

  3. Run an Event Storming session Bring domain experts and developers together for a few hours. Map the events, mark the hot spots and draw candidate boundaries.

  4. Draw your context map Document the relationships that already exist, including the uncomfortable ones (conformist, shared database). Making them visible is the first step to fixing them.

  5. Protect the core with an ACL Identify the legacy or external integration that leaks the most into your core model and wrap it in an anti-corruption layer.

  6. Enforce boundaries before distributing Start with a modular monolith or strict module boundaries, one schema per context and architecture tests. Extract services only when there's a concrete reason.

  7. Align teams with contexts Review who owns what. Every context should have exactly one owning team, with a cognitive load it can handle.

Comments

Questions, corrections, or your own take are all welcome. Sign in with GitHub to join in.