Skip to main content

Command Palette

Search for a command to run...

Part 8. Versioning Modules Independently Inside a Single Deployment

Updated
5 min read
Part 8. Versioning Modules Independently Inside a Single Deployment

One of the quiet assumptions people carry into modular monoliths is that versioning only becomes a problem once you go distributed. While everything lives in one deployment, the thinking goes, you can just change things together and move on.

That assumption holds only while change is cheap and coordinated. The moment different parts of the system start evolving at different speeds, the absence of a versioning strategy turns every change into a negotiation.

This is not a microservices problem. It’s a time problem.

In a real modular monolith, modules do not mature evenly. Some stabilise quickly and barely change. Others sit close to the business edge and churn constantly. Treating them as if they all move together is how you end up with artificial constraints and unnecessary coupling.

The first thing to be clear about is what we mean by “versioning” in this context. We are not talking about NuGet packages or semantic version numbers published to the world. We are talking about the ability for one module to change its behaviour or contracts without forcing immediate changes in every consumer.

That sounds abstract until you see the alternative.

Imagine a Users module that exposes a query returning user details. At first, it looks like this.

public sealed record UserSummary(Guid Id, string Email);

Billing consumes it. Reporting consumes it. Everything is fine.

Then the Users module evolves. The business now distinguishes between primary and secondary email addresses. The model changes.

public sealed record UserSummary(
    Guid Id,
    string PrimaryEmail,
    IReadOnlyList<string> SecondaryEmails
);

If this change ripples through the entire system immediately, you don’t have independent evolution. You have coordinated refactoring. In a small codebase that might be acceptable. In a system that’s growing, it becomes a tax you pay over and over again.

The simplest and most reliable way to version inside a monolith is not through numbers, but through coexistence. Old behaviour stays available while new behaviour is introduced alongside it.

Nothing forces every consumer to move at once. Migration happens incrementally, with intent.

In practice, this usually means versioning contracts, not modules.

The Users module still deploys as one unit. It still owns its data. But it exposes more than one contract shape for a period of time.

That might look like this in code.

public interface IUserQueriesV1
{
    Task<UserSummaryV1> GetAsync(Guid userId);
}

public interface IUserQueriesV2
{
    Task<UserSummaryV2> GetAsync(Guid userId);
}

Both are implemented internally by the Users module. Both are valid. One is newer. This feels slightly uncomfortable at first, because it introduces duplication. That discomfort is the cost of time. You’re paying it explicitly instead of smearing it across the system.

What matters is that versioning lives at the boundary, not in the core. Internally, the module should have one clear model and one clear understanding of the business. The translation to older or newer shapes happens at the edge. That keeps the complexity contained.

Feature flags are often suggested as an alternative to versioning, and they do have a place here, but only if you’re honest about what they are doing. A feature flag that permanently changes behaviour for different consumers is not a flag. It’s a version switch disguised as configuration.

Used carefully, flags help you stage a change. Used carelessly, they become a second, hidden versioning system that nobody fully understands.

The rule I follow is simple. Flags are temporary. Versions are explicit. If a conditional is going to live longer than a release cycle or two, it should probably be a versioned contract instead.


While we’re talking about feature flags, it’s probably worth being transparent about something. A lot of the opinions in this section come from having been burned by flag sprawl more than once, which is why I’ve been building a small feature flag platform called Flagmesh on the side.

The goal isn’t to turn flags into a second versioning system or bury business logic behind configuration, but to make it obvious what flags exist, why they exist, and when they should be removed. If you ever find yourself needing feature flags and want something deliberately opinionated about scope and cleanup, it’s there. If not, the principles still stand regardless of tooling.


Another place versioning shows up is in events. This is where people often get caught out, because events feel immutable until they aren’t. An event is a promise. Once it’s published, consumers will build assumptions around it. Changing it silently is one of the fastest ways to break trust between modules.

When an event needs to evolve, the safest approach is the most boring one, publish a new event type.

public sealed record UserCreatedV1(Guid UserId, string Email);
public sealed record UserCreatedV2(
    Guid UserId,
    string PrimaryEmail,
    IReadOnlyList<string> SecondaryEmails
);

Both can coexist. Producers can emit both for a while if needed. Consumers can migrate on their own schedule. This is not wasteful. It is respectful of time and autonomy. Inside a single deployment, this approach has an important benefit. You can see who is still using the old contract. You can log it. You can measure it. You can remove it deliberately when the time is right. Nothing is implicit. Nothing breaks “by accident”.

One subtle but important point is that independent versioning only works if your modules are already decoupled in the ways described earlier in the series. If consumers are reaching into internals, or sharing DbContexts, or coordinating synchronously, versioning becomes performative. You can put a V2 suffix on things, but the coupling is still there. This is why versioning belongs after boundaries, slices, data ownership, and communication patterns. It’s not a starting point. It’s a capability you earn.

A modular monolith that supports independent versioning is a system that understands it will live longer than its first set of assumptions. It doesn’t cling to the idea that everything must always move together. It accepts that change is uneven and builds around that reality.

That acceptance is what keeps the system supple instead of brittle.


In the next part of the series, the focus shifts from evolution to operation. Once modules evolve independently, you need to be able to see, diagnose, and manage them independently as well. Logging, metrics, health, and failure handling become the difference between confidence and guesswork.