Domain-Driven Design: Drawing the Boundaries

July 21, 2026 · 23 min read

GreenBox delivers weekly produce boxes from local farms. With 1,000 subscribers and a team growing from five to fifteen, the startup is expanding to Melbourne – and the codebase that was built fast for a small team is starting to crack under the weight.

Maya is chopping sweet potato when her phone rings. She’s at home in Fremantle, Saturday evening, Nadia’s Spotify playlist filling the kitchen. Nadia is making a dressing at the counter. The number on the screen is Dave Morrison’s.

Dave doesn’t call on weekends. He doesn’t call much at all – he’s a text message man, and even those are sparse. Three words. “Zucchini looks short.” “Carrots fine this week.” Maya puts down the knife and answers.

“Maya. That Freshly mob rang me today.”

He says it the way he says everything: flat, unhurried, like he’s reporting rainfall. Maya leans against the counter. Nadia glances over.

“They’re offering guaranteed volume,” Dave continues. “A hundred crates a week. That’s more than you take from me in a month.”

Maya’s mouth goes dry. “Are you going to switch?”

A pause. Dave doesn’t rush pauses. “I didn’t say that. I said they called. Thought you should know.”

They talk for another two minutes. Dave mentions that Rachel got the same call. He doesn’t say what Rachel’s thinking. He says “goodnight, Maya” and hangs up.

Maya puts the phone face-down on the counter. Nadia has stopped whisking.

“What happened?”

“The thing I was afraid of.”

Nadia waits. She’s good at that – better than Maya, who fills silences like they’re leaking. Maya tells her about Freshly: the VC-funded produce box company from Sydney, the $12 million in funding, the guaranteed volume they’re dangling in front of the farms that GreenBox depends on. She’s known about Freshly since a churned subscriber mentioned them months ago. She’s been watching their Perth expansion from a distance. But a phone call to Dave is different. That’s not a competitor entering a market. That’s someone reaching for the thing she built.

“What are you going to do?” Nadia asks.

“I don’t know yet. But I need to move faster.”

The sweet potato burns slightly while they talk. They eat it anyway.


GreenBox has a thousand subscribers. Let that sink in for a moment. The produce box startup that was scrambling to hit 200 subscribers six months ago now has a thousand people paying for weekly deliveries. They’re opening operations in Melbourne. Maya has signed on three new farms in the Yarra Valley. And the team is growing from five people to fifteen.

The codebase that Tom and Priya built for 200 subscribers is groaning under the weight.

It was never designed for this. It was designed for a small team that knew everything about the system, sitting in one room in Perth, building features as fast as they could to prove the business model worked. And it did work. Brilliantly. But the code carries every shortcut, every “we’ll fix this later,” every assumption baked in during those early sprints.

Charlotte noticed it on her second day.

Charlotte is the senior agile coach Maya brought on to help with the scaling challenges. She’s spent fifteen years working with subscription businesses – meal kits, SaaS platforms, logistics companies. She’s seen what happens when a startup codebase meets rapid growth, and she’s seen it go wrong more often than she’d like.

Lee is still around. He helped the team build their discovery foundations – Event Storming, Example Mapping. Those foundations are solid. But the problems GreenBox faces now are different. They’re not “we don’t understand the domain” problems. They’re “the domain is fine but the architecture can’t keep up” problems. Charlotte’s territory.

The pull request that said everything

Kai joins the team on a Monday. He’s twenty-eight, from Sydney, five years at a fintech company where he built payment systems that handled half a billion dollars a year. Solid Go skills, comfortable with LLMs, and he carries himself with the quiet confidence of someone who has never worked on a codebase he couldn’t master in a week. He left the fintech job because, as he told Maya in the interview, “I wanted to work on something I could explain to my mum.” His mum buys produce from Paddy’s Markets in Sydney every Saturday. She understood GreenBox immediately.

Maya hired him specifically to build the gift subscription feature that customers have been requesting since Christmas.

With Kai joining, Tom realises the deploy script doesn’t scale. Two developers deploying from their laptops at the same time caused a conflict last week – Kai’s changes overwrote Tom’s. Tom sets up a basic CI/CD pipeline: tests run automatically, and deploys go through a single pipeline instead of individual laptops. It’s not sophisticated – there’s one staging environment and deploys still take twenty minutes – but at least they’re sequential and tested. Priya’s GitHub Action from the BDD work evolves into a real pipeline.

Kai reads the codebase over Monday and Tuesday. He’s fast – Tom notices him scrolling through packages with the focused efficiency of someone who’s done this dozens of times. By Tuesday afternoon Kai is asking pointed questions in Slack about the subscription model that Tom has to think about before answering. On Wednesday, he opens his LLM and prompts: “Add a gift subscription feature to this codebase. A customer should be able to buy a subscription as a gift for someone else. The gift recipient gets an email, creates an account, and starts receiving boxes.”

The LLM generates code. Lots of code. Kai reviews it, tweaks a few things, writes some tests, and opens a pull request on Thursday afternoon.

The PR touches 47 files.

Forty-seven. Across every part of the system. The subscription model, the payment processing, the delivery scheduling, the farm matching algorithm, the email templates, the customer portal. The gift subscription feature reaches into every corner of the codebase because the codebase has no corners. It’s one big room.

One of the changes modifies the farm matching algorithm – it assumes supply is reliable enough to serve gift recipients on the same schedule as regular subscribers. Dave Morrison, whose zucchini yield over-promises by twenty percent every spring, would have something to say about that assumption. But Dave isn’t in the code review. The gap between farm reality and developer assumptions, the same gap that caused problems in the very first weeks, is alive and well at a thousand subscribers.

Tom reviews the PR and his heart sinks. Not because the code is bad – it’s actually well-written. Better than well-written. Some of the function signatures are cleaner than his own. But every change is tangled with everything else. Changing how gift subscriptions are billed requires touching the same files that handle regular billing. The delivery scheduling changes affect all subscribers, not just gift recipients. The farm matching modifications could break the substitution logic that Maya spent weeks getting right.

Tom stares at the diff for forty minutes. He wrote most of this codebase. He knows where the bodies are buried – every shortcut, every late-night compromise, every function he’d meant to refactor “next sprint.” Kai’s PR has reached into all of them. Not maliciously. Inevitably. The codebase invited it.

“I can’t review this,” Tom tells Kai honestly. “Not because it’s wrong. Because I can’t tell what it’ll break.”

Charlotte is sitting nearby. She pulls up the PR on her screen, scrolls through the file list, and says: “This is a symptom, not a bug.”

What Charlotte sees

Charlotte has seen this pattern dozens of times. A codebase that started small and grew by accretion. Features bolted on. Concepts blurred together. The word “subscription” meaning one thing in the payment code, another thing in the delivery code, and a third thing in the customer-facing UI.

She asks the team a question: “When you say ‘subscription,’ what do you mean?”

Tom says: “The record in the database that tracks what box size someone gets and when they’re billed.”

Priya says: “The relationship between a customer and their delivery schedule.”

Sam says: “The thing a customer signs up for and can pause or cancel.”

Maya says: “The commitment to receive a box every week.”

Four people. Four definitions. None of them wrong. All of them different.

“That’s your problem,” Charlotte says. “Not four definitions – four definitions living in one codebase with no boundaries between them. When Kai asked the LLM to add gift subscriptions, the LLM did exactly what the codebase told it to do: it spread the feature across everything, because everything is connected to everything.”

Domain-Driven Design

Charlotte introduces the team to Domain-Driven Design – specifically, the concept of Bounded Contexts. DDD is a set of ideas from Eric Evans’ book of the same name. It’s a big book and a deep topic, but the core insight Charlotte wants the team to grasp is this: complex systems should be divided into distinct areas, each with its own clear language and its own clear boundaries.

A Bounded Context is one of those areas. Inside a bounded context, terms have precise, consistent meanings. The same term can mean different things in different contexts – and that’s fine, as long as the boundary is clear.

“Subscription” in the billing context means “a recurring charge.” In the fulfilment context, it means “a delivery schedule.” In the customer context, it means “a thing I signed up for.” These aren’t contradictions. They’re different perspectives on the same business concept, and they belong in different parts of the code.

Charlotte pulls up the Event Storm photographs from months ago. The wall of sticky notes is still there – Maya had them photographed and laminated, which Charlotte says is one of the smartest things she’s seen a founder do. She means it. The meal kit company Charlotte coached before GreenBox – the one that failed, the one she doesn’t talk about – never photographed a single workshop. When Charlotte started there, the team of thirty couldn’t explain why the pricing model worked the way it did. There was no institutional memory, no record of the decisions that shaped the business. She’d sworn she’d never let that happen again.

“Look at the clusters,” Charlotte says. She draws lines on a whiteboard, grouping the domain events into four areas:

Subscription Context
  • Customer Subscribed
  • Subscription Paused
  • Subscription Cancelled
  • Gift Subscription Created
  • Box Size Changed
Billing Context
  • Payment Charged
  • Payment Failed
  • Invoice Generated
  • Refund Issued
Supply Matching Context
  • Farm Availability Submitted
  • Supply Matched to Demand
  • Substitution Applied
  • Shortfall Detected
Fulfilment Context
  • Box Packed
  • Box Dispatched
  • Delivery Confirmed
  • Delivery Failed

Subscription → Billing, Subscription → Supply Matching, Supply Matching → Fulfilment, Billing → Fulfilment

Four bounded contexts. Subscription, Billing, Supply Matching, Fulfilment. Each one has its own language, its own responsibilities, its own reason to change.

“When a customer subscribes,” Charlotte explains, “the Subscription context creates a subscription. It publishes an event. The Billing context hears that event and sets up recurring charges. The Supply Matching context hears it and adds the subscriber to the demand pool. The Fulfilment context doesn’t care until there’s a box to pack.”

“Each context talks to the others through events and clearly defined interfaces. Inside each context, the code is self-contained. You can change how billing works without touching fulfilment. You can change how supply matching works without touching subscriptions.”

The language problem

Charlotte spends an hour with the team mapping out the language in each context. This is what DDD calls the Ubiquitous Language – the shared vocabulary within a single bounded context.

In the Subscription context: a “subscription” is a customer’s ongoing relationship with GreenBox. It has a status (active, paused, cancelled), a box size, and a start date. A “gift subscription” is a subscription purchased by one customer for another.

In the Billing context: a “subscription” is a billing schedule. It has a payment method, a charge amount, a billing cycle. It doesn’t know or care whether the subscription is a gift. It just charges the card on file.

In the Supply Matching context: there are no “subscriptions” at all. There’s “demand” – the total number of boxes needed for the week, broken down by size. It doesn’t matter whether that demand comes from regular subscribers, gift recipients, or one-off purchases.

In the Fulfilment context: there are “orders” – individual boxes to be packed and delivered to specific addresses. The fulfilment context doesn’t know about subscriptions. It knows about boxes, addresses, and delivery dates.

“This is why Kai’s PR was 47 files,” Charlotte says. “The codebase uses ‘subscription’ everywhere, for everything. The LLM followed that convention. It generated code that treated gift subscriptions the same way – spreading the concept across the entire system.”

Tom nods slowly. “So if we had these boundaries, the LLM would…”

“Try it,” Charlotte says.

The reprompt

Kai opens his LLM again. This time, Charlotte helps him write the prompt differently.

Instead of “add a gift subscription feature to this codebase,” he writes:

Add a gift subscription to the Subscription context. A gift subscription is created by a purchasing customer for a recipient. It has a status (pending, activated, active, paused, cancelled), a box size, a purchaser reference, and a recipient email. When a gift subscription is created, publish a GiftSubscriptionCreated event. When the recipient activates it, publish a GiftSubscriptionActivated event. The Subscription context does not handle billing, delivery, or supply matching – those are separate contexts that will respond to the events.

The LLM generates code. Kai reviews it. The PR touches 8 files. All of them in the subscription package.

Eight files instead of forty-seven. One package instead of the entire codebase. Tom can review it in twenty minutes.

“That’s the difference,” Charlotte says. “The boundary didn’t just organise the code. It organised the conversation with the LLM. When you tell the LLM to work within a bounded context, it stays inside the boundary. When you don’t, it does what the codebase does – which is everything, everywhere, all at once.”

The Context Map

The bounded contexts don’t exist in isolation. They need to communicate. A customer subscribing in the Subscription context needs to trigger billing in the Billing context and add demand in the Supply Matching context.

Charlotte introduces the team to a Context Map – a diagram showing how the bounded contexts relate to each other and what flows between them.

Context Map: Events Between Bounded Contexts
From To Events
Subscription Billing SubscriptionCreated, SubscriptionPaused, SubscriptionCancelled
Subscription Supply Matching SubscriptionCreated, SubscriptionCancelled
Supply Matching Fulfilment BoxAllocated, SubstitutionApplied
Billing Fulfilment PaymentConfirmed

The arrows show events flowing between contexts. Each context publishes events that other contexts can subscribe to. The Subscription context doesn’t call the Billing context directly – it publishes a SubscriptionCreated event, and the Billing context decides what to do with it.

This loose coupling means the team can work on different contexts simultaneously. Kai can build gift subscriptions in the Subscription context while Priya works on the Melbourne delivery zones in the Fulfilment context. Their code won’t collide because it lives in different bounded contexts.

Charlotte asks the LLM to generate the integration code – the event handlers that wire the contexts together. She feeds it the Context Map and the event definitions. The LLM produces clean, focused integration code because it knows exactly what flows between contexts and what each context’s responsibility is.

“Context Maps aren’t just for humans,” Charlotte says. “They’re for LLMs too. The more explicit you are about boundaries and responsibilities, the better the generated code.”

Tom’s resistance

Tom pushes back. He’s been quiet for most of the session, and now he speaks up.

“Look, I get the theory. But this feels like Java-enterprise-architect nonsense. We’re a startup. We have fifteen people, not fifteen hundred. We don’t need microservices and message buses and all that ceremony. We need to ship features.”

Charlotte doesn’t dismiss him. “You’re right about the ceremony. DDD has a reputation for being over-engineered, and in big enterprises, it often is. People build elaborate domain models with seventeen layers of abstraction for a CRUD app. That’s a waste.”

She pauses.

“But look at Kai’s PR. Forty-seven files. Could you review it?”

“No.”

“Could you be confident it wouldn’t break existing billing?”

“No.”

“Could you explain to a new joiner what it does?”

Tom is quiet.

“That’s the problem DDD solves at your scale. Not the enterprise problem of coordinating a thousand developers. The practical problem of a growing team working in a codebase where everything is tangled. Bounded contexts aren’t about ceremony. They’re about being able to change one thing without breaking everything else.”

She shows him the numbers. Average PR size before bounded contexts: 23 files. Average PR size after, in the first week of working this way: 9 files. Review time dropped by more than half. And the LLM-generated code required fewer corrections because the prompts were more focused.

Tom looks at the numbers. “Fine. But if I ever have to write a UML diagram, I’m quitting.”

“Deal,” Charlotte says.

That evening, Tom sits in his home office after the kids are asleep. Three monitors, the framed print of his first merged PR, LEGOs on the floor from where Leo was playing after dinner. Sarah comes in with a cup of tea.

“You’re quiet tonight.”

“Charlotte wants to carve up the codebase. Draw boundaries. Restructure the whole thing into separate contexts.”

“Is she right?”

Tom doesn’t answer immediately. He’s looking at the PR diff still open on his centre monitor – Kai’s 47 files, the code that touched everything he’d built.

“Yeah. Probably. It’s just –” He picks up a LEGO brick from the desk, turns it over. “I built this. All of it. And now someone’s telling me it needs walls.”

Sarah leans against the door frame. “You love making things. But you hate letting anyone help you make them. You’re like your dad.”

Tom’s jaw tightens. His father runs a construction company. Marco, Tom’s brother, works there. Every family dinner, Marco talks about the business and Tom’s dad listens like it matters. When Tom explains what he does, his dad says “that’s great, mate” and changes the subject.

“That’s not fair,” Tom says.

“It’s not a criticism. It’s an observation.” Sarah picks up Leo’s LEGO from the floor. “The codebase isn’t yours any more, Tom. It’s theirs. That’s what growing means.”

She leaves the tea and goes to bed. Tom stares at the monitor for another hour.

Making it real

The team spends the rest of the week refactoring the codebase to align with the bounded contexts. This isn’t a big-bang rewrite – Charlotte is adamant about that. “You refactor incrementally. Move one piece at a time. Keep shipping.”

They start with the clearest boundary: Billing. The billing code is already somewhat isolated because it talks to Stripe, and Stripe has its own clear API. They extract it into its own package, define the events it listens for and publishes, and update the integration points.

The LLM helps with the refactoring. Charlotte has the team write prompts like: “Extract all billing-related code from the subscription package into a new billing package. The billing package should listen for SubscriptionCreated events and handle recurring charges. It should not reference customer preferences, delivery schedules, or farm matching.”

The LLM produces the extraction cleanly because the instruction is bounded. It knows what to move and what to leave behind.

The following week, they do Supply Matching. Then Fulfilment. By the end of the third week, the four contexts are in place. Not perfectly – there are still some leaky abstractions and a few places where contexts know too much about each other. But the major boundaries are drawn.

The boundaries that don’t stick

Two weeks after the DDD session, Kai opens another PR. He’s been building the gift subscription activation flow – what happens when a gift recipient clicks the link in their email, creates an account, and starts receiving boxes. The PR touches the Subscription context (creating the account), the Billing context (setting up the gifter’s payment for the recipient’s deliveries), and the Fulfilment context (scheduling the first box).

Three bounded contexts. One feature.

Tom looks at the PR and says, to nobody in particular: “I told you it wasn’t that simple.”

Charlotte doesn’t defend the diagram. She pulls up the whiteboard where the four contexts are drawn and studies the event flows. The gift activation genuinely requires coordination between subscriptions, billing, and fulfilment. The feature isn’t violating the boundaries – the boundaries were drawn in the wrong place.

“You’re right,” Charlotte says to Tom. “The boundaries I drew were a first hypothesis. Let’s redraw them based on what we’ve learned.”

The team spends an afternoon reworking the context map. The Subscription and Billing contexts share too many events for gift subscriptions to sit cleanly in either one. They merge them into a single “Commercial” context that owns everything about the customer relationship: subscriptions, billing, gifts, and pausing. Supply Matching and Fulfilment stay separate – they’re genuinely independent.

Tom watches Charlotte erase her own lines and draw new ones. He’d expected her to defend the original design. She doesn’t. She treats the first map the way Lee treated the first Event Storm – as a starting point, not a finished product.

“DDD is iterative,” Charlotte says, stepping back from the redrawn map. “The first set of boundaries is always wrong. You find out where they’re wrong by building against them and seeing what fights you. Kai’s PR told us something about the domain that the workshop didn’t.”

Kai, who has been quiet, looks at the new map. “So the 47-file PR was useful after all.”

Charlotte smiles. “The 47-file PR was the most expensive domain discovery session GreenBox ever ran. But yes. It was useful.”

New team members joining over the following weeks can be pointed at a single context. “You’re working on Supply Matching. Here’s the package. Here are the events it publishes and consumes. You don’t need to understand Billing or Fulfilment to be productive.” Onboarding time drops from two weeks to a few days.

The restructuring comes with its own pain. When Charlotte introduces bounded contexts, Tom realises the database needs splitting too. The subscription table has columns for billing, delivery, and farm matching all mixed together. His first migration takes the site down for twenty minutes on a Sunday morning while he moves columns between tables. Three subscribers email Sam: “Why was the site down?” Tom: “We need to learn to migrate without downtime.” Priya researches zero-downtime migrations – dual writes, backfill, switch. Their next migration, weeks later when the audit log table is added, goes live without anyone noticing. Progress.

When the business changes

A month later, Maya announces that GreenBox is launching a corporate catering service – weekly fruit boxes for offices. It’s a different product with different pricing, different delivery schedules, different fulfilment requirements.

In the old codebase, this would have been a nightmare. Corporate subscriptions would have been tangled with individual subscriptions, corporate billing with individual billing, corporate delivery with home delivery. Every change would touch everything.

With bounded contexts, Charlotte helps the team think about it clearly. “Which contexts need to change?”

Subscription: yes, there’s a new type of subscription. But it’s still a subscription – a customer’s ongoing relationship with GreenBox. The Subscription context gains a “corporate” subscription type alongside “individual” and “gift.”

Billing: yes, corporate billing is different – monthly invoicing instead of weekly charges. But that’s a billing concern. It stays in the Billing context.

Supply Matching: barely changes. Corporate demand is just more demand. The matching algorithm doesn’t care whether a box is going to a house or an office.

Fulfilment: yes, corporate delivery has different requirements – bulk drops to a single address instead of individual deliveries. That’s a fulfilment concern.

Each context changes independently. The changes stay local. The team works in parallel. Nobody’s PR touches 47 files.

“DDD isn’t about perfection,” Charlotte says at the Friday retro. “It’s about making the architecture match the business so that when things change – and they will – the change stays local. You don’t have to understand the whole system to change one part of it. And neither does the LLM.”

What the team learned

Bounded contexts are not an architectural luxury. They’re a practical tool for managing complexity in a growing codebase. When the team was five people who all sat in one room and understood every line of code, boundaries were unnecessary overhead. At fifteen people, with new joiners arriving monthly and LLMs generating code at speed, boundaries are the thing that keeps the codebase comprehensible.

The technique is straightforward. Identify the natural divisions in your domain. Draw the boundaries. Define the language within each boundary. Specify how the boundaries communicate. Then enforce those boundaries in the code – separate packages, clear interfaces, events between contexts.

The payoff is immediate and measurable: smaller PRs, faster reviews, better LLM output, easier onboarding.

Charlotte’s final point for the week: “Every time you prompt an LLM, you’re making an architectural decision. If your prompt says ‘add this feature to the codebase,’ you’ll get a 47-file PR. If your prompt says ‘add this feature to the Subscription context,’ you’ll get an 8-file PR. The boundary isn’t just in the code. It’s in the conversation.”

Next, the team tackles a different kind of complexity. The substitution rules that Maya carries in her head need to scale to Melbourne – and to a team member who doesn’t have twenty years of farming knowledge. Charlotte reaches for decision tables (coming 11 August).

Questions or thoughts? Get in touch.