An engineering approach to modularity instead of subdomain divination

An engineering approach to modularity instead of subdomain divination

How many times have you sat at a DDD workshop, staring at a board full of colorful sticky notes and wondering: "Is this really a subdomain, or just a large class?" Or how many times has your Bounded Context simply turned out to be a distributed monolith?
Although Domain-Driven Design is a great way to think about architecture, its tools, such as subdomains and heuristics for finding them, are often too vague to make engineering-verifiable decisions. That's why we need to abandon philosophy in favour of physics: the goal is modifiability, and the method is simulation of changes. Stop guessing, start proving!

What are we actually optimising?

If you have worked in the industry for more than a year, you will know that nobody will pay you for "low coupling" or "high cohesion". These architectural features are not the goal, but the means. The true goal is the ability to introduce changes quickly and safely – in short, modifiability.
As an engineer, your job is not to create "clean architecture", but to create code that minimises the cost of changes. When a manager asks why something takes so long, you can't answer: "Because we have high coupling". Instead, you should be able to say: 'Thanks to our architecture, we implement changes of this type twice as fast, but in this area we need refactoring because each change requires the involvement of three teams.'
In programming engineering, as in civil engineering, we must work with quantifiable quality attributes, not features. This is the key to dialogue with the business.
The boundary of the model is a decision, not a fact. The traditional approach assumes that you first identify the subdomain (Problem Space), and then create the bounded context (Solution Space) based on it, often with a 1:1 mapping.

I propose an engineering small revolution:

  1. Discovering Capability: Identify the fragments of the domain that change independently. Let's call them Business Capabilities. These are the things you need to discover in the domain.
  2. Designing Context: A bounded context (BC) is a model boundary. The number of capabilities you include in one BC is a conscious design decision, influenced by your current quality attributes (e.g. budget, time to market, required scalability).

For example, a startup with a time-to-market driver may intentionally close three capabilities (e.g. "Ordering", "Payment Verification", "Reporting") in one large BC. Why? Because it minimises integration costs and enables quick product validation. Later, when scalability becomes key, the BC can be broken down into smaller pieces. The decision is conscious and justified.

Simulation instead of heuristics

How to find these independently changing fragments, if not through subdomain divination? Use simulation.
Simulation involves creating (or taking from the backlog) extreme variants of requirements, and then checking whether a change in one area can affect another. If change X does not affect Capability Y, the boundary is stable.
This is exactly what we do in TDD: we simulate behaviour before writing code. This is what an aeronautical engineer does: they simulate a wing failure before the plane takes off. We simulate future changes before implementing the architecture.

Example of a simulation in action

Let's say you have a system to support ordering and payments.We want to check the boundary between Order Management (order management) and Fraud Prevention (fraud prevention).
If the boundary is weak (if there is too much overlap in the model), a change in one area will require changes in the other.

# The Order Management Context class, using an Order model
class OrderManagement
  def process_order(order_data)
    # Logic to create and persist the order
    order = Order.new(order_data)
    order.save

    # Check for fraud synchronously - BAD IDEA in this context!
    if FraudService.check_risk(order.customer_id, order.total) > 0.9
      order.status = 'FRAUD_SUSPECT'
    end

    # ... more logic
  end
end

In the above example, if the Fraud Prevention team changes the way risk is calculated (e.g. by adding a new field to the Order model for analysis), the Order Management team must change their class because they use their logic. The boundary is breached.
Simulation of change: What if the Fraud Prevention team had to start supporting new, exotic payment methods that required adding ten new fields to the model?
The result of the simulation is that the change is propagated (leaking). This is proof that BC is not sufficiently independent.

Engineering solution: stable contract

In an engineering approach, we stabilise the contract between modules so that they are connected only by a pivotal event – a minimal, stable event describing a business fact.

# The Order Management Context is now decoupled from Fraud Prevention
class OrderManagement
  def process_order(order_data)
    order = Order.new(order_data)
    order.save

    # Publish a minimal, stable event for others to react to
    EventBus.publish(OrderPlaced.new(order.id, order.customer_id, order.total_amount))

    # No synchronous fraud checking here! The context is clean.
  end
end

# The Fraud Prevention Context reacts to the stable event
class FraudPrevention
  def handle_order_placed(event)
    # The event contains only the minimal data needed for processing (ID, total, customer)
    # Changes to internal fraud logic do NOT impact OrderManagement.
    risk = calculate_complex_risk(event.customer_id, event.total_amount)

    if risk > 0.9
      # Publish a new event or command
      CommandBus.send(FlagOrderForReview.new(event.order_id))
    end
  end
end

In the new version, changes to the Fraud Prevention logic (e.g. the addition of geolocation analysis) require changes only within the context of FraudPrevention. The boundary is stable and has been proven by simulation.

Communication with the business

Remember that the final architecture (e.g. one monolith with modules or 10 microservices) is only a means to an end. When talking to the business, avoid jargon. Communicate value through quality attributes.

  1. Instead of saying: "We need to refactor because the aggregate is too big"
    Say: "We need to invest time in refactoring because currently the onboarding time for a new engineer on this module is 6 weeks, but we want to reduce it to 2 weeks."
  2. Instead of saying: "Let's extract this as a new DDD microservice."
    Say: "Splitting this into two services will enable us to implement this type of change 75% faster, as we will reduce the number of teams involved in a deployment from four to one."

This is the language that convinces the business. Engineering is not just about code, but also about managing trade-offs in line with verifiable goals.

Happy DDDing!