Language-Driven Design
BookCHAPTER ELEVEN | Meaning Split Pattern
Chapter 0420 min read

CHAPTER ELEVEN | Meaning Split Pattern

"We thought we were designing software. We were actually designing language."

Table of Contents
Meaning ProblemThe Problem Before the ProblemA Word Is Not a ConceptContextThe Promotion That Wasn’t One ThingThe First Rule of Meaning SplitFrom One Meaning to SeveralWe split PromotionThe Big Tent Anti-PatternHow the Big Tent Appears in CodeThe Type Field Is Often a ConfessionThe Model Is LyingThe SolutionStep One: Catch the WordStep Two: Collect the DefinitionsStep Three: Look for Behavioral DifferencesStep Four: Look for Different LifecyclesStep Five: Name the MeaningsThe Strange Thing About SimplicityThe RefactoringDo Not Split the Database FirstThe Real Test: ConversationWhat Makes This Different from Ubiquitous Language?Meaning Split Is Not Bounded Contexts EitherA More Interesting Example: OrderWhen Should You NOT Split?The Three QuestionsQuestion OneQuestion TwoQuestion ThreeA Useful Smell: “It Depends”The Hidden ArchitectureSoftware Architecture is Frozen LanguageThe Most Important Part: The Old Word Must Lose Its JobA Practical Migration StrategyA Small Test That Reveals a Big ProblemThe Database Tells the Same StoryMeaning Split and Semantic DebtWhy Rewriting Does Not Fix ThisThe Difference Between Splitting and RenamingWhen the Old Word Should SurviveA Useful Mental ModelForcesShared VocabularyHistorical CodeOrganizational BoundariesVocabulary ExplosionMigration CostA Decision HeuristicThe Pattern in One PageNameIntentContextProblemForcesAnti-PatternSolutionResultRiskTestRelated PatternsSemantic BoundaryLanguage ClosureUbiquitous LexiconState/Status SegregationVocabulary ObjectsBehavior as DataA Final ExampleThe Deeper PointOne Last Question

Meaning Split Pattern

“When a word tries to be too many things, it stops being anything at all. Split it before it splits your system.”

Meaning Problem

There is a particular kind of software problem that is surprisingly difficult to see.

The code compiles. The tests pass. The database is responding normally. The architecture diagram looks reasonable enough, and nobody has opened an incident because of it.

Yet every time someone wants to change something, the conversation becomes strangely complicated.

Someone says: “But that’s not what Promotion means here.”

Someone else answers: “It is a Promotion. We have always called it that.”

Then somebody opens the code and finds three different classes, two database tables, four status fields, and a collection of if statements that all contain the word Promotion.

Nothing is technically broken. And yet everybody knows something is wrong.

I have seen this kind of problem often enough that I no longer think of it as a naming problem. It is a meaning problem.

And once a word starts carrying several meanings, changing the name is usually the easy part. The difficult part is discovering that there were several concepts hiding behind that one name in the first place. That is what this pattern is about. I call it Meaning Split.

The important word here is not “split.” It is “meaning.”

We are not splitting a class because it has become too large. We are not splitting a service because somebody read an article about microservices. We are not splitting a table because thirty-seven columns look ugly.

We are discovering that one thing in the language has quietly become several different things in the minds of the people building the system.

Then we make that difference explicit.

The Problem Before the Problem

For years, I thought the interesting architectural questions started with things like:

Where should this functionality live? Should this be a separate service? Should these entities belong to the same bounded context? Should we introduce another module? Should this be an aggregate?

Those are useful questions, of course.

But there is a question that comes before all of them.

What exactly are we talking about?

That sounds almost too simple to be worth asking. It isn’t.

Consider the word Order. An Order might be the thing a customer places on a website. It might be the commercial commitment recorded by Finance. It might be the collection of items a warehouse needs to pick. It might be the request that a delivery operation needs to fulfill. It might even be the legal record that Compliance needs to retain We tend to say that all of these are “the same Order” because they are related. But related does not mean identical. And this is where things become interesting.

Suppose five people use the word Order.

The product manager says an Order is created when the customer confirms the checkout.

The finance person says an Order becomes real when payment is authorized.

The warehouse manager says an Order is something that can be picked.

The delivery team says an Order is something that can be dispatched.

The legal team says an Order is a record of a commercial transaction.

Who is wrong? Probably nobody. That is the uncomfortable part. The problem is not that one team misunderstood the word. The problem is that we are asking one word to serve five different conversations. Eventually the code has to represent all five conversations, and that is where the trouble becomes visible.

The architecture gets blamed because it is the thing we can see. The language is harder to see because it was never drawn on the architecture diagram.

But it was there first.

A Word Is Not a Concept

This distinction took me a surprisingly long time to appreciate. A word and a concept are not the same thing. A word is a linguistic handle. It is something we use to point at an idea.

The problem starts when several ideas happen to use the same handle. Imagine five boxes sitting on a table. They all have the same label:

Promotion

You open the first one and find a marketing campaign. You open the second and find a discount rule. The third contains an execution of a campaign. The fourth contains a legal offer. The fifth contains a report about promotions. The label did not tell you what was inside.

It only told you that someone, at some point, decided that all five things could be called Promotion. That is Concept Overload.

And Concept Overload is not simply “a bad name.” A bad name can be corrected without changing the model. Concept Overload is different. The model itself has started to contain multiple concepts under one linguistic identity.

That distinction matters because the solution is not necessarily:

“Let’s rename Promotion to MarketingPromotion.”

Sometimes that works. Sometimes it merely gives the garbage can a nicer label.

Meaning Split asks a harder question:

Are we actually looking at one concept, or have we been pretending that several concepts are one because they happen to share a word?

Context

Meaning Split becomes useful when a concept has accumulated distinct meanings over time and those meanings have begun to affect the design of the system.

You will usually encounter it in a system that has lived long enough for language to evolve without anyone explicitly governing it. A new requirement arrives.

Someone discovers that an existing concept is almost what they need. They add a property. Another team needs something slightly different. They add another property. A third team needs another behavior. Someone adds a Type.

Eventually the code still has one class, but the people using it no longer agree on what the class represents. That is the context.

The system does not necessarily look unhealthy at first. In fact, the opposite can happen. The original model may have been beautifully simple. The first version of Promotion might have been completely correct.

The problem is that the business changed, while the language was allowed to remain frozen.

The old word survived. The old abstraction survived. Only the meaning changed. That is a dangerous combination.

The Promotion That Wasn’t One Thing

Let us make this concrete. Imagine an e-commerce company. They have a concept called Promotion. The first version is simple:

public class Promotion
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public decimal PercentageOff { get; set; }
    public DateTime StartDate { get; set; }
    public DateTime EndDate { get; set; }
}

The business rule is straightforward. A promotion gives customers a discount during a particular period. Nobody is unhappy. Then Marketing arrives. They want campaigns.

A campaign needs a budget, a target audience, a communication plan, and a maximum number of customers. The easiest thing to do is to extend Promotion.

public class Promotion
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public decimal PercentageOff { get; set; }
    public DateTime StartDate { get; set; }
    public DateTime EndDate { get; set; }
    public decimal? CampaignBudget { get; set; }
    public string? TargetAudience { get; set; }
    public int? MaximumParticipants { get; set; }
}

Still not terrible. Then Pricing arrives.

They say: “We need tiered discounts.”

Now a Promotion can mean:

  • Buy one item and receive 5 percent.
  • Buy three and receive 10 percent.
  • Buy five and receive 15 percent.

So the class grows again.

public class Promotion
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public decimal? PercentageOff { get; set; }
    public decimal? FixedAmountOff { get; set; }
    public decimal? CampaignBudget { get; set; }
    public string? TargetAudience { get; set; }
    public int? MaximumParticipants { get; set; }
    public int? MinimumQuantity { get; set; }
    public int? MaximumQuantity { get; set; }
    public DateTime StartDate { get; set; }
    public DateTime EndDate { get; set; }
}

Now Operations arrives. They do not care about campaign budgets or discount percentages. They need to know whether the promotion execution is currently running.

So somebody adds:

public PromotionExecutionStatus ExecutionStatus { get; set; }

Then Compliance asks for approval information. Finance asks for accounting information. Marketing asks for campaign analytics. The class keeps growing. Eventually someone notices that most properties are nullable.

That should be our first clue. Not just because nullable properties are inherently bad. Because we should ask why the object needs so many properties that are irrelevant depending on what somebody means by Promotion. Eventually we reach something like this:

public class Promotion
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    // Marketing
    public decimal? CampaignBudget { get; set; }
    public string? TargetAudience { get; set; }
    public int? MaximumParticipants { get; set; }
    // Pricing
    public decimal? PercentageOff { get; set; }
    public decimal? FixedAmountOff { get; set; }
    public int? MinimumQuantity { get; set; }
    // Operations
    public PromotionExecutionStatus? ExecutionStatus { get; set; }
    public DateTime? StartedAt { get; set; }
    public DateTime? CompletedAt { get; set; }
    // Finance
    public string? AccountingCode { get; set; }
    public decimal? RecognizedAmount { get; set; }
    // Compliance
    public string? RegulatoryClassification { get; set; }
    public DateTime? ApprovedAt { get; set; }
    public DateTime StartDate { get; set; }
    public DateTime EndDate { get; set; }
}

At this point, I would not ask the team to refactor the class. I would ask a much more dangerous question.

What is a Promotion?

Not “what fields does it have?” Not “what does the database call it?” What is it?

That question usually produces much better information.

Marketing might answer:

“It is a campaign that we run to influence customer behavior.”

Pricing might answer:

“It is a rule that determines how much discount applies.”

Operations might answer:

“It is an execution we monitor from start to finish.”

Finance might answer:

“It is something that creates a financial adjustment.”

Compliance might answer:

“It is a commercial offer that has to satisfy certain regulatory rules.”

Now we have a problem. But notice something important.

We have not found a naming problem yet. We have found a conceptual problem. That distinction is the whole point of this pattern.

The First Rule of Meaning Split

When different people give different definitions of the same term, do not immediately conclude that you need a new term.

That would be too easy. Language naturally contains ambiguity. The word “bank” can mean a financial institution or the side of a river. Nobody needs a RiverBank refactoring workshop every time somebody says “bank.” (ok, I see that the example is so weird!)

The question is whether the difference in meaning has consequences for the system. Meaning Split becomes interesting when different interpretations lead to different:

  • rules,
  • behaviors,
  • lifecycles,
  • data,
  • ownership,
  • decisions,
  • invariants,
  • or architectural boundaries.

If two people use slightly different explanations but make exactly the same decisions, you may simply have a wording problem.

If they use the same word while making incompatible decisions, you probably have a meaning problem. That is the distinction I look for.

From One Meaning to Several

Let us return to our Promotion.

Suppose Marketing says:

“A promotion is active when today’s date falls between the campaign start and end dates.”

Pricing says:

“A promotion is active when the discount rule is enabled and its conditions can currently produce a discount.”

Operations says:

“A promotion is active when an execution is running successfully.”

Those three definitions are not variations of the same rule. They are three different concepts. And now we can see something that was invisible before.

The word active was overloaded too. This is where semantic problems become interesting.

We split Promotion

Then we discover that Active may need splitting. Then we discover that Status may need splitting. Then we discover that Product means something different in the campaign and pricing models.

This is not accidental chaos. It is what happens when one word was hiding several concepts.

The Big Tent Anti-Pattern

I call the common failure mode The Big Tent. A Big Tent concept begins with something reasonable. The original concept is useful, so people naturally want to extend it.

Someone says:

“Can we just add this to Promotion?”

Sure.

Then another person says:

“Can we also use Promotion for this?”

Sure.

Then someone else says:

“It is basically the same thing.”

And that sentence is dangerous.

“Basically the same thing” is one of the most expensive phrases in software design. Sometimes two things really are the same. Sometimes “basically” is doing all the work.

The tent keeps growing because every new addition is close enough to the existing concept that nobody feels justified in introducing another one.

Eventually the concept becomes a collection of things that are related but no longer identical. That is the Big Tent.

The problem with a Big Tent is not that it contains too much code. The problem is that the tent hides the fact that the people underneath it are doing different things.

How the Big Tent Appears in Code

The most obvious symptom is a Type. You have probably seen something like this:

public class Promotion
{
    public Guid Id { get; set; }
    public PromotionType Type { get; set; }
    public string Name { get; set; }
    public decimal? PercentageOff { get; set; }
    public decimal? Budget { get; set; }
    public DateTime? StartDate { get; set; }
    public DateTime? EndDate { get; set; }
}
public enum PromotionType
{
    Campaign,
    Discount,
    Execution
}

At first glance, this looks perfectly reasonable. We have polymorphic behavior represented with a type. Except that now the code has to remember which properties are meaningful for which type.

public decimal CalculateValue(Promotion promotion)
{
    switch (promotion.Type)
    {
        case PromotionType.Discount:
            return promotion.PercentageOff ?? 0;
        case PromotionType.Campaign:
            return promotion.Budget ?? 0;
        case PromotionType.Execution:
            return promotion.PercentageOff ?? 0;
        default:
            throw new ArgumentOutOfRangeException();
    }
}

The type field is telling us something important. The code is already treating Promotion as several things. We have simply refused to admit it in the model. The split exists. It is just implicit.

This is one of the strongest signals for Meaning Split:

When the code repeatedly checks what kind of thing something is, ask whether the language already knows that these are different things.

The Type Field Is Often a Confession

I am not saying that every enum called Type is a design smell. That would be silly. Sometimes an entity genuinely has types. The question is what the type means.

Consider:

public enum PaymentMethod
{
    Card,
    BankTransfer,
    Cash
}

There is nothing suspicious here. These are genuinely different types of payment method. But now imagine:

public enum PromotionType
{
    Campaign,
    Discount,
    Execution
}

This is different. We are not distinguishing variants of one concept. We are distinguishing concepts that participate in different conversations. A campaign has a budget. A discount rule has pricing conditions. An execution has a runtime lifecycle.

The type field is not describing one concept. It is hiding three concepts behind one class. That is a much stronger candidate for Meaning Split.

The Model Is Lying

This is perhaps my favorite way to recognize the problem.

The model says: “I am one thing.”

The code says: “Sometimes.”

The database says: “It depends.”

The teams say: “Actually, we mean different things.”

When all four disagree, believe the teams. The model is lying. And the lie can survive for years because the code still works. This is why semantic problems are so dangerous.

A compiler cannot detect them. A unit test usually cannot detect them.

A static analyzer cannot tell you that Marketing and Pricing are using the same word for fundamentally different concepts. You need to notice it. That is why Language-Driven Design treats language as design material.

The Solution

Meaning Split is simple to describe.

When one term represents multiple concepts with materially different meanings, separate those meanings and give them distinct names.

The important part is “materially different.” We are not trying to create a new class for every slight variation in vocabulary. We are looking for conceptual divergence. The process has several stages.

First, discover the overloaded term. Second, collect the different meanings without trying to solve them immediately. Third, compare those meanings and look for differences in rules, behavior, lifecycle, ownership, and invariants. Fourth, name the concepts independently. Fifth, test the new language in conversation.

Only then should you start changing the code.

This order matters. If you start with the code, you are likely to produce a technical decomposition that preserves the original ambiguity. We are trying to change the language first. The code follows.

Step One: Catch the Word

The easiest place to begin is conversation.

Listen for sentences such as:

“In our case, Promotion means…”

“For Finance, Promotion is…”

“Technically it is a Promotion, but…”

“Here Promotion has a different status.”

“This Promotion is not the same as the Marketing Promotion.”

That last sentence should make you stop. If people regularly need an adjective to rescue a word from ambiguity, the adjective may be telling you that the noun needs splitting.

MarketingPromotion.

OperationalPromotion.

CustomerPromotion.

InternalPromotion.

Sometimes the adjective is simply useful. Sometimes it is evidence.

Step Two: Collect the Definitions

Do not immediately argue about which definition is correct. That is usually a waste of time. Write them down.

For our example:

Team Meaning of Promotion Important Rules
Marketing Campaign Budget, audience, dates
Pricing Discount Rule Discount calculation, eligibility
Operations Promotion Run Execution lifecycle
Finance Financial Adjustment Accounting and recognition
Compliance Commercial Offer Regulatory constraints

Now the problem is visible. The word was giving us the illusion of unity. The table shows the opposite. There are several concepts.

Step Three: Look for Behavioral Differences

This is where Meaning Split goes beyond simple terminology management.

Ask: If these two meanings were really the same concept, would they behave the same way?

Consider active.

For a Campaign:

public bool IsActive(DateTime now)
{
    return StartDate <= now &&
           now < EndDate;
}

For a DiscountRule:

public bool IsActive(PricingContext context)
{
    return Enabled &&
           IsEligible(context) &&
           !LimitReached;
}

For a PromotionRun:

public bool IsActive()
{
    return Status == ExecutionStatus.Running;
}

Look at what happened. We started with one word: Active

But the behavior reveals three different concepts. The same English word is now hiding three different predicates. That is exactly the kind of divergence Meaning Split is designed to expose.

Step Four: Look for Different Lifecycles

Different lifecycles are particularly strong evidence. A campaign might have:

Draft -> Scheduled -> Running -> Finished

A discount rule might have:

Disabled -> Enabled -> Suspended

An execution might have:

Queued -> Running -> Completed -> Failed

People might casually call all of them “Promotion Status.” But they are not the same lifecycle. Trying to force them into one state machine is not simplification. It is information loss.

This is one reason Meaning Split often works together with the State/Status Segregation Pattern.

Once the overloaded concept is split, its state and process status can often be modeled independently. That is not coincidence. Language problems tend to travel in groups.

Step Five: Name the Meanings

Now comes the part people usually think is the whole exercise. It isn’t. Naming is important, but naming is the consequence of understanding.

For our Promotion example, we might arrive at:

public class CampaignOffer
{
    public CampaignId Id { get; init; }
    public string Name { get; init; }
    public decimal Budget { get; init; }
    public Audience TargetAudience { get; init; }
    public DateRange CampaignPeriod { get; init; }
}

The pricing concept becomes:

public class DiscountRule
{
    public DiscountRuleId Id { get; init; }
    public string Name { get; init; }
    public DiscountPolicy Policy { get; init; }
    public EligibilityRule Eligibility { get; init; }
    public DiscountLimit Limit { get; init; }
}

The operational concept becomes:

public class PromotionRun
{
    public PromotionRunId Id { get; init; }
    public DiscountRuleId RuleId { get; init; }
    public ExecutionStatus Status { get; private set; }
    public DateTime StartedAt { get; private set; }
    public DateTime? CompletedAt { get; private set; }
}

Notice something. The code is not necessarily smaller. That is intentional. Meaning Split is not a code minimization technique.

You may end up with more classes, more tables, and more vocabulary. The system can become physically larger while becoming conceptually smaller. That is a trade worth understanding.

The Strange Thing About Simplicity

Software engineers have a natural attraction to fewer things. One class feels simpler than three classes. One table feels simpler than three tables. One enum feels simpler than three enums. One word certainly feels simpler than three words. But fewer things are not necessarily simpler.

Suppose I give you one box containing: A hammer, a laptop, three oranges, a database schema, a pair of shoes, and a guitar. Technically, that is one object.

Would you call it simpler than having six clearly labeled places for those things? Probably not.

The number of containers is not the same as the amount of complexity. The same thing happens with software models. A single Promotion class can contain more conceptual complexity than three clearly named concepts. This is one of the reasons semantic debt is so easy to accumulate. We optimize for visible simplicity while quietly increasing invisible complexity.

The Refactoring

Once the language is stable enough, we can change the code. Suppose the original system has:

public class Promotion
{
    public Guid Id { get; set; }
    public PromotionType Type { get; set; }
    public decimal? CampaignBudget { get; set; }
    public decimal? PercentageOff { get; set; }
    public PromotionExecutionStatus? ExecutionStatus { get; set; }
    public DateTime? StartDate { get; set; }
    public DateTime? EndDate { get; set; }
}

We do not have to rewrite everything overnight. We can introduce the new concepts alongside the old one.

For example:

public class CampaignOffer
{
    public Guid Id { get; init; }
    public decimal Budget { get; init; }
    public DateRange Period { get; init; }
    public Audience TargetAudience { get; init; }
}

And:

public class DiscountRule
{
    public Guid Id { get; init; }
    public DiscountPolicy Policy { get; init; }
    public EligibilityRule Eligibility { get; init; }
}

And:

public class PromotionRun
{
    public Guid Id { get; init; }
    public Guid DiscountRuleId { get; init; }
    public ExecutionStatus Status { get; private set; }
}

Then the old model can temporarily act as a compatibility boundary.

public class LegacyPromotionAdapter
{
    public CampaignOffer ToCampaignOffer(Promotion promotion)
    {
        return new CampaignOffer
        {
            Id = promotion.Id,
            Budget = promotion.CampaignBudget
                ?? throw new InvalidOperationException(
                    "Promotion has no campaign budget.")
        };
    }
    public DiscountRule ToDiscountRule(Promotion promotion)
    {
        return new DiscountRule
        {
            Id = promotion.Id,
            Policy = DiscountPolicy.FromPercentage(
                promotion.PercentageOff
                    ?? throw new InvalidOperationException(
                        "Promotion has no discount policy."))
        };
    }
}

The point is not the adapter itself. The point is that the ambiguity now has a visible place. We know that the old Promotion object is legacy. We know that CampaignOffer means one thing. We know that DiscountRule means another. We know that PromotionRun means something else again. The language has changed. The code is catching up.

Do Not Split the Database First

This is an important practical detail. When people discover a conceptual split, they often immediately start discussing tables. “Let’s create three tables.” Maybe. But that is not the first step.

The first step is deciding whether there are three concepts. Otherwise you are simply moving the ambiguity around. You can have three tables and still have one broken word.

campaign_offers

discount_rules

promotion_runs

looks much better than:

promotions

with thirty-seven nullable columns.

But if everybody still calls all three things “Promotion,” the semantic problem has not actually been solved. The database is cleaner. The language is still broken.

And because architecture is frozen language, the ambiguity will eventually find its way back into the architecture.

The Real Test: Conversation

Here is the test I care about most. After the split, sit in a conversation where the concepts matter.

Ask:

“Should this CampaignOffer be eligible for this DiscountRule?”

If people understand the question immediately, good.

Ask:

“Can a PromotionRun exist without a DiscountRule?”

Again, good.

Now ask:

“Is this Promotion active?”

If everyone starts asking what you mean by Promotion, congratulations. You have discovered that the old word is still alive. This is where many refactorings fail. The code gets renamed, but the language does not. Developers continue saying “Promotion.”

Product people continue saying “Promotion.” Documentation still says “Promotion.” The Jira tickets say “Promotion.” The database column is still called PromotionId.

Six months later, someone creates another class called Promotion. The old meaning comes back.

This is why Meaning Split is a language pattern rather than merely a refactoring technique.

The goal is not to change identifiers. The goal is to change what the team thinks the system contains.

What Makes This Different from Ubiquitous Language?

At this point, someone familiar with Domain-Driven Design may reasonably ask: “Isn’t this just Ubiquitous Language?” No. At least, I do not think it is!

Ubiquitous Language gives us an important principle: the language of the domain should be shared by the people working on the model, and that language should appear in the software.

Meaning Split starts with a different observation. It is specifically interested in the moment when the shared language itself becomes too coarse to represent the domain accurately.

DDD tells us to use language deliberately.

Meaning Split gives us a way to recognize one particular failure in that language and act on it.

The difference becomes clearer with an example. Suppose a team agrees that Order is an important domain concept. They establish it as part of their Ubiquitous Language.

Good.

But after two years, the word Order is being used by Sales, Finance, Fulfillment, and Compliance to represent four different concepts. Saying “we have a Ubiquitous Language” does not solve that problem. The language is shared. It is also wrong.

Meaning Split asks us to investigate the shared word itself. Should there still be one Order?

Or are there several concepts that have been compressed into the same linguistic symbol? That is a different question. And I think it is an important one.

Meaning Split Is Not Bounded Contexts Either

There is another tempting interpretation.

Someone might say:

“Fine. Different meanings belong in different bounded contexts. This is just bounded context discovery.”

Again, there is a connection, but they are not the same thing. A bounded context is a boundary within which a particular model and its language have meaning.

Meaning Split can happen before we know what the boundaries should be. In fact, it can help us discover them.

Suppose we start with: Promotion. We discover: CampaignOffer, DiscountRule, PromotionRun. Only now can we start asking better architectural questions.

Does CampaignOffer belong with the campaign management model?

Does DiscountRule belong with pricing?

Should PromotionRun belong with an execution or orchestration model?

Should they communicate through events?

Which concepts are shared?

Which concepts should remain independent?

Those are architectural questions. Meaning Split does not answer them.

It gives us a better vocabulary with which to ask them.

That distinction matters. The pattern does not say:

“Split every overloaded word into bounded contexts.”

It says:

First determine whether you actually have one meaning. Then decide what architectural relationship those meanings should have.

Architecture comes after the language has become explicit.

A More Interesting Example: Order

Promotion is useful because it makes the problem easy to see. Order is harder. And that is precisely why it is worth examining.

Imagine an online retailer. The customer places an order.

At checkout:

public Order PlaceOrder(Customer customer, Cart cart)
{
    return new Order(
        customer.Id,
        cart.Items,
        cart.Total);
}

Simple enough. Then Finance needs an order for accounting.

Warehouse needs an order for picking.

Shipping needs an order for dispatch.

Customer Support needs an order for customer communication.

Eventually the model starts accumulating things:

public class Order
{
    public Guid Id { get; set; }
    public Guid CustomerId { get; set; }
    public decimal Total { get; set; }
    public PaymentStatus PaymentStatus { get; set; }
    public InvoiceStatus InvoiceStatus { get; set; }
    public FulfillmentStatus FulfillmentStatus { get; set; }
    public ShipmentStatus ShipmentStatus { get; set; }
    public CancellationStatus CancellationStatus { get; set; }
    public RefundStatus RefundStatus { get; set; }
    public ComplianceStatus ComplianceStatus { get; set; }
    public DateTime CreatedAt { get; set; }
}

At some point someone says: “We have too many statuses.” Maybe.

But that may not be the fundamental problem. The deeper problem might be that Order is being asked to represent several different concepts.

A commercial order. A payment obligation. An invoice. A fulfillment request. A shipment. A refund.

The code has not necessarily failed. The language has.

And because the language has failed, the code has become the place where everybody tries to store their interpretation.

When Should You NOT Split?

This pattern is dangerous if applied mechanically.

If every difference in meaning becomes a new concept, you will end up with a vocabulary nobody can remember.

Imagine a team saying:

“We cannot say Customer anymore. In this sentence we need PurchasingCustomer, BillingCustomer, RegisteredCustomer, SupportCustomer, CustomerProfile, CustomerAccount, CustomerParty, CustomerIdentity, and CustomerRecord.”

At that point, congratulations. You have replaced ambiguity with vocabulary hell. Meaning Split is not an argument for maximum vocabulary. It is an argument for accurate vocabulary.

So before splitting, ask whether the difference actually affects the system. If two meanings produce the same rules, same lifecycle, same behavior, same ownership, and same decisions, they may still be one concept.

If the difference is merely a different perspective on the same stable concept, splitting may make things worse.

A concept can legitimately be seen from several perspectives. Not every perspective deserves a new noun.

The Three Questions

When I suspect Concept Overload, I usually ask three questions.

Question One

Would these two things behave differently?

If yes, keep investigating.

Question Two

Would the rules governing them be different?

If yes, the case becomes stronger.

Question Three

Would I make a different decision depending on which meaning I intended?

This is the one I find most useful.

Suppose someone asks:

“Is this Promotion active?”

If the answer changes depending on whether we mean the marketing campaign, the pricing rule, or the execution, then we have something important.

The ambiguity is no longer linguistic decoration.

It affects behavior.

That is where Meaning Split becomes valuable.

A Useful Smell: “It Depends”

There is a phrase I listen for in architecture discussions. “It depends.”

Not every “it depends” is a problem. Software is full of context. But when someone asks:

“Is the Promotion active?”

and the answer is:

“It depends on what you mean by active.”

I want to know why. Then someone says:

“Well, Marketing considers it active if the campaign dates are valid, but Pricing considers it active if the rule is enabled, and Operations considers it active if the execution is running.”

Now we have something concrete. The word active is hiding multiple predicates. The word Promotion may be hiding multiple concepts.

And the architecture may already be reflecting those differences through conditional logic, modules, tables, or services. The language has simply failed to name them.

The Hidden Architecture

This is where Meaning Split connects directly to the central thesis of Language-Driven Design.

Software Architecture is Frozen Language

Look again at the original model.

public class Promotion
{
    public PromotionType Type { get; set; }
    public decimal? CampaignBudget { get; set; }
    public decimal? PercentageOff { get; set; }
    public PromotionExecutionStatus? ExecutionStatus { get; set; }
}

The architecture looks like one concept. But the code contains: Campaign, Discount, Execution.

The type field is effectively an architectural diagram written as code. It is telling us:

“There are several things here.”

We simply chose not to give them independent names. The architecture has already frozen the language. It has frozen a bad decision.

After Meaning Split, the architecture becomes easier to discuss because the language becomes more precise.

CampaignOffer (influences) => DiscountRule (creates) => PromotionRun.

Now the relationships can be discussed. Before the split, everything was just Promotion.

The architecture was not simple. It was mute.

The Most Important Part: The Old Word Must Lose Its Job

A Meaning Split is incomplete until the old overloaded word stops being the default word. This sounds trivial. It is not. Suppose we introduce:

CampaignOffer, DiscountRule, PromotionRun.

but the team keeps saying:

“Can you update the Promotion?”

The old abstraction is still alive. Soon somebody creates:

public class PromotionService
{
}

And then:

public class PromotionRepository
{
}

And:

public class PromotionStatus
{
}

And six months later we are back where we started. The old word has become a magnet. This is why I consider vocabulary migration part of the pattern.

You need to change:

  • code,
  • documentation,
  • API terminology,
  • database terminology where appropriate,
  • diagrams,
  • tickets,
  • tests,
  • conversations,
  • and sometimes team names.

The last one is usually where the battle is won or lost.

If the team naturally starts saying DiscountRule instead of Promotion when discussing pricing, the split has become real. If they need a glossary every time they speak, it probably has not.

A Practical Migration Strategy

You do not need to stop the company for three months and announce a “Semantic Migration Program.” Please don’t.

Start with one meaningful path through the system. Suppose Pricing is the first area you want to separate. Introduce:

public sealed class DiscountRule
{
    public DiscountRuleId Id { get; }
    public DiscountPolicy Policy { get; }
    public EligibilityRule Eligibility { get; }
    public DiscountRule(
        DiscountRuleId id,
        DiscountPolicy policy,
        EligibilityRule eligibility)
    {
        Id = id;
        Policy = policy;
        Eligibility = eligibility;
    }
}

Then change one use case:

public class CalculateDiscount
{
    public Money Execute(
        DiscountRule rule,
        PricingContext context)
    {
        if (!rule.Eligibility.IsSatisfiedBy(context))
            return Money.Zero;
        return rule.Policy.Calculate(context);
    }
}

Notice how much easier this is to read. There is no:

promotion.Type

There is no:

if (promotion.Type == PromotionType.Discount)

There is no nullable campaign budget. There is no execution status. The code is not magically more elegant. It is simply expressing a concept that has a name. That is what language-driven design looks like in practice.

A Small Test That Reveals a Big Problem

Here is another practical technique. Take the overloaded concept and try to write its tests separately. For example:

[Fact]
public void Campaign_is_active_during_campaign_period()
{
    // ...
}

Then:

[Fact]
public void Discount_rule_is_active_when_enabled_and_eligible()
{
    // ...
}

Then:

[Fact]
public void Promotion_run_is_active_while_execution_is_running()
{
    // ...
}

Look at the test names. They are telling you something.

We started with: Promotion_IsActive**()**and ended up with three tests that have completely different meanings.

This is not merely a testing improvement. The tests are exposing the linguistic fracture. In that sense, tests can become semantic instruments. They can help us discover that the model is pretending to represent one concept while the behavior already contains several.

The Database Tells the Same Story

Go back to the old table.

CREATE TABLE Promotions
(
    Id UNIQUEIDENTIFIER NOT NULL,
    Name NVARCHAR(200) NOT NULL,
    CampaignBudget DECIMAL(18,2) NULL,
    TargetAudience NVARCHAR(200) NULL,
    PercentageOff DECIMAL(5,2) NULL,
    FixedAmountOff DECIMAL(18,2) NULL,
    ExecutionStatus INT NULL,
    StartedAt DATETIME2 NULL,
    CompletedAt DATETIME2 NULL,
    AccountingCode NVARCHAR(50) NULL,
    RegulatoryClassification NVARCHAR(100) NULL
);

The nullable columns are not necessarily the problem. They are evidence. Ask why these columns coexist. If the answer is:

“Because different kinds of Promotion use different fields.” then the database is telling us what the language has failed to say. The table contains several concepts. The schema has frozen the overloaded language.

After the split, we might have: CampaignOffers, DiscountRules, PromotionRuns

The schema becomes more explicit. But again, the database migration is not the pattern.

The semantic distinction is the pattern. The database is simply one of the places where the distinction becomes visible.

Meaning Split and Semantic Debt

I introduced the idea of Semantic Debt earlier in this book because I kept seeing a particular kind of debt that did not look like technical debt.

Technical debt often comes from shortcuts in implementation. Semantic debt comes from shortcuts in meaning. The original decision may have been perfectly reasonable.

“Let’s call this Promotion.” Fine.

The debt appears when we continue using that word after the business has evolved beyond the concept it originally represented. Every additional interpretation increases the debt. Every new API that exposes the overloaded term increases it. Every new table that uses the same terminology increases it. Every developer who learns the wrong abstraction increases it. Eventually the organization is spending time translating between meanings. That is an invisible tax.

Meaning Split is one way of paying down that tax.

Why Rewriting Does Not Fix This

This is worth saying explicitly because I have watched teams make this mistake. Suppose the old system is terrible. You decide to rewrite it. The new architecture is beautiful.

New framework. New database. New APIs. New services. You copy the requirements.

You copy the terminology. You create:

public class Promotion
{
}

Congratulations.

You have successfully rebuilt the semantic debt. The old system was not necessarily teaching you the wrong technology. It was teaching you the wrong language. If the meaning remains overloaded, the new architecture will eventually reproduce the same fracture.

This is why I do not think semantic problems are solved by rewriting. You can rewrite the implementation. You cannot rewrite the meaning accidentally. You have to design it.

The Difference Between Splitting and Renaming

This distinction deserves its own section.

Suppose we have:

public class Promotion
{
}

and we rename it:

public class MarketingPromotion
{
}

Have we performed Meaning Split? Not necessarily.

We may have simply renamed the original concept. Meaning Split requires us to identify the concepts that were previously compressed into one term. If there was only one concept, we performed a rename. If there were three concepts and we renamed the class to MarketingPromotion while leaving the other two meanings elsewhere, we have only partially addressed the problem.

For example:

Promotion

  • MarketingPromotion
  • DiscountPromotion
  • OperationalPromotion

This might still be wrong if the three concepts are not really “types of Promotion.”

Perhaps they are: Campaign, DiscountRule, PromotionRun.

The latter is much stronger because it does not preserve the original abstraction unnecessarily.

This is an important principle:

When splitting meaning, do not preserve the old concept merely because it is familiar.

Sometimes the correct result is not three kinds of the old thing. Sometimes the old thing disappears entirely.

When the Old Word Should Survive

There are cases where the original word remains useful.

Suppose Marketing has: Campaign, Pricing has: DiscountRule, Operations has: PromotionRun.

There may still be a higher-level business concept called Promotion that describes the relationship among them.

For example:

public sealed class Promotion
{
    public CampaignOffer Campaign { get; }
    public DiscountRule DiscountRule { get; }
    public PromotionRun Run { get; }
}

That could be perfectly valid. The important thing is that Promotion now has a clear meaning. It is no longer being used interchangeably with Campaign, DiscountRule, or PromotionRun.

The problem was never that a word existed at multiple levels. The problem was that nobody knew which level they were talking about. This is another reason Meaning Split should not be treated as a mechanical renaming exercise. We are not trying to eliminate words. We are trying to eliminate accidental ambiguity.

A Useful Mental Model

I find it useful to imagine language as a map. A word is a point on that map.

When everyone uses the word to refer to roughly the same region, navigation is easy.

But suppose the word starts pointing to several distant regions.

Campaign

*

|

|

Promotion *—–+

|

|

*

DiscountRule

At first, the distance may be small. Then another concept appears.

Campaign

*

|

|

Promotion *—–+———* DiscountRule

|

|

*

PromotionRun

Eventually the word is no longer pointing to one place. It is pointing to a whole map. At that point, saying “Promotion” does not help navigation. It makes navigation harder.

Meaning Split gives those places their own names. Now: CampaignOffer, DiscountRule, PromotionRun. Each word points somewhere specific. The vocabulary has become larger. The map has become easier to navigate. That is the trade.

Forces

Meaning Split is not free. Several forces make it difficult.

Shared Vocabulary

Teams naturally prefer fewer words.

A shared term creates a feeling of alignment. If everyone says Promotion, it feels like everyone agrees. Splitting the term can initially feel like creating fragmentation. The irony is that the fragmentation may already exist. You are not creating three concepts. You are revealing three concepts that were already there.

Historical Code

The existing code has momentum. There are APIs. There are database records. There are reports. There are integrations. There are tests.

There are people who have worked with Promotion for five years. You cannot simply announce that the word is dead.

Organizational Boundaries

Different teams may have invested in their interpretation of the concept. Marketing may feel that “Promotion” belongs to them. Pricing may feel the same. Operations may have built an entire workflow around it. A semantic split can therefore become an organizational conversation. That is another reason to focus on meaning rather than ownership.

The question is not:

“Which team owns Promotion?”

The better question is:

“What concepts are actually present here?”

Vocabulary Explosion

Every split introduces more words. That is useful until it isn’t. If the new vocabulary becomes harder to understand than the original ambiguity, you have gone too far.

Migration Cost

Changing language across a living system takes time. The cost is real. Meaning Split does not pretend otherwise. The decision is whether the cost of changing the language is lower than the cost of continuing to operate with the ambiguity. That is an engineering decision.

A Decision Heuristic

When I am unsure whether to split a concept, I use a simple heuristic. Ask the people using the term to define it independently. Do not let them negotiate the answer first.

Collect the definitions. Then compare them. If the differences are superficial, keep the concept. If the differences change behavior, investigate further. If the differences change rules, lifecycle, ownership, or invariants, you probably have a strong candidate for Meaning Split.

Then try the new names in conversation before changing the architecture. If people immediately understand the distinction, you have probably discovered something real. If everyone struggles to use the new terms, stop. Perhaps the split was artificial. Perhaps you have not understood the domain well enough yet. Language should become more precise, not more impressive.

The Pattern in One Page

Name

Meaning Split

Intent

Separate distinct concepts that have become hidden behind a single overloaded term.

Context

A system contains a word that is used across different conversations, teams, or parts of the model, while the meanings attached to that word have begun to diverge.

Problem

One term is carrying several materially different meanings, causing the model, code, data, and architecture to represent multiple concepts as though they were one.

Forces

Shared vocabulary favors keeping one term. Existing code makes change expensive. Different contexts produce legitimate interpretations. Semantic debt increases as the ambiguity survives. Excessive splitting can create unnecessary vocabulary and fragmentation.

Anti-Pattern

The Big Tent

A concept keeps expanding until unrelated or materially different meanings are placed underneath the same linguistic abstraction.

Solution

Identify the distinct meanings, verify that their differences affect behavior or rules, give the meanings explicit names, establish those names in conversation, and then allow the code and architecture to follow the new language.

Result

The vocabulary becomes larger but more precise. Models become more explicit. Conditional logic based on conceptual type often decreases. Architectural boundaries become easier to reason about because the concepts they contain have clearer meanings.

Risk

Splitting too aggressively can create unnecessary concepts and vocabulary.

Test

Ask whether the new concepts naturally appear in real conversations and whether they lead to different rules, behavior, lifecycle, or decisions.

Meaning Split rarely appears alone. It is often the beginning of a sequence.

Semantic Boundary

Once two meanings have been separated, we need to prevent them from silently collapsing back together.

Meaning Split creates the distinction.

Semantic Boundary protects it.

This is why the two patterns belong close to each other.

Language Closure

After introducing new concepts, the vocabulary can start growing again.

Language Closure provides the discipline for deciding what belongs in the language and what does not.

Ubiquitous Lexicon

The new terms have to become part of the shared language.

Otherwise Meaning Split remains an exercise performed by a few engineers while the rest of the organization continues using the old word.

State/Status Segregation

Meaning Split often reveals that what looked like one status is actually several different notions of state and process.

Once the concepts are separated, their lifecycles can often be modeled independently.

Vocabulary Objects

Sometimes the split reveals that a supposedly simple primitive is actually carrying a meaningful concept.

A string called PromotionType may eventually become an explicit object with rules and behavior.

Behavior as Data

When meanings are separated, behavior that was previously buried inside conditionals can sometimes be represented explicitly as data associated with the appropriate concept.

This is particularly useful when the system has accumulated many rules around one overloaded abstraction.

A Final Example

Let us return to the original class.

We started with this:

public class Promotion
{
    public Guid Id { get; set; }
    public PromotionType Type { get; set; }
    public decimal? CampaignBudget { get; set; }
    public decimal? PercentageOff { get; set; }
    public PromotionExecutionStatus? ExecutionStatus { get; set; }
    public DateTime? StartDate { get; set; }
    public DateTime? EndDate { get; set; }
}

The class looked convenient. But convenience was hiding the real model.

After Meaning Split:

public sealed class CampaignOffer
{
    public CampaignId Id { get; }
    public Money Budget { get; }
    public Audience Audience { get; }
    public DateRange Period { get; }
}

Pricing gets its own concept:

public sealed class DiscountRule
{
    public DiscountRuleId Id { get; }
    public EligibilityRule Eligibility { get; }
    public DiscountPolicy Policy { get; }
    public DiscountLimit Limit { get; }
}

Operations gets its own concept:

public sealed class PromotionRun
{
    public PromotionRunId Id { get; }
    public DiscountRuleId RuleId { get; }
    public ExecutionStatus Status { get; private set; }
    public DateTime StartedAt { get; private set; }
    public DateTime? CompletedAt { get; private set; }
    public void Start(DateTime time)
    {
        if (Status != ExecutionStatus.Scheduled)
            throw new InvalidOperationException(
                "Only scheduled runs can be started.");
        Status = ExecutionStatus.Running;
        StartedAt = time;
    }
    public void Complete(DateTime time)
    {
        if (Status != ExecutionStatus.Running)
            throw new InvalidOperationException(
                "Only running executions can be completed.");
        Status = ExecutionStatus.Completed;
        CompletedAt = time;
    }
}

Now look at what happened. We did not merely make the classes prettier. We changed the questions the code allows us to ask.

Before:

promotion.Status

What does that mean?

After:

campaign.Period
discountRule.Policy
promotionRun.Status

The language itself tells us what we are dealing with. That is the real outcome.

The Deeper Point

The most interesting thing about Meaning Split is that the final code is almost beside the point. Yes, the code becomes clearer. Yes, the database may become healthier. Yes, the architecture may eventually acquire better boundaries. But none of those things is where the pattern starts. It starts with somebody noticing that a word has become suspiciously convenient.

A word that everyone uses. A word that appears everywhere. A word that seems to make communication easier while actually forcing everyone to explain what they mean every time they use it. That is the paradox.

The most familiar word in a system can sometimes be the word causing the most confusion.

We usually think that shared vocabulary creates alignment. It can. But shared vocabulary without shared meaning creates something much worse: the illusion of alignment.

Everyone says the same word. Everyone thinks they agree. The architecture quietly records the disagreement. Then, years later, somebody asks why changing one small thing requires touching seven services. Sometimes the answer is not hidden in the code. It is hidden in a word.

One Last Question

Think about the system you are working on right now.

Do not start with its architecture diagram.

Pick one word. Maybe it is Customer. Maybe it is Order. Maybe it is Account. Maybe it is Transaction. Maybe it is Policy.

Ask five people what it means. Do not help them. Do not explain the existing model. Just ask. Then write their answers down.

If you get five versions of the same idea, congratulations. Your language may be healthy.

If you get five different concepts hiding behind the same word, do not immediately create five classes.

Sit with the difference. Look at the rules. Look at the behavior. Look at the lifecycle. Look at the decisions. Look at the code. You may discover that your system has already been split semantically. The architecture simply has not caught up yet.

And that is the moment Meaning Split becomes useful.

Not when the code is ugly. Not when the class is too large. Not when someone tells you to apply another design pattern. When you realize that the thing you thought was one thing has quietly become several. Give those things names. Let the language tell the truth. Then let the architecture follow.

Because if architecture is frozen language, sometimes the best architectural refactoring begins with something that looks almost embarrassingly small:

choosing a better word.