Runtime Composition Patterns in Modular Monoliths

⏱ 20 min read

Most architecture failures do not begin with the wrong technology. They begin with the wrong seams.

A team starts with good intentions: one deployable unit, shared codebase, clear module boundaries, maybe even a disciplined package structure. They call it a modular monolith, and for a while it behaves beautifully. Delivery is fast. Debugging is humane. Transactions are simple. Then the enterprise shows up in full. Product lines diverge. Regional rules split. Sales wants tenant-specific behavior. Compliance wants stricter isolation around a handful of domains. One business capability needs to move faster than the rest. Another must be integrated with Kafka. A third is halfway to becoming a service, but not today. event-driven architecture patterns

This is the moment where many teams make an expensive mistake. They confuse modularity with composition. Modularity gives you structure. Composition gives you runtime variation. Without runtime composition, a modular monolith often becomes a beautifully organized lump: well-named packages, clean diagrams, and a deployment that still forces every customer, every business line, and every workflow through the same execution path.

That is a bad trade.

The practical question is not whether the modular monolith is “good” or whether microservices are “better.” The practical question is this: how do we compose domain behavior at runtime without destroying domain semantics, testability, or migration options?

That is where runtime composition patterns matter. They let you assemble capabilities inside a monolith the way a city grows around roads rather than around a single building plan. The roads are stable. The neighborhoods vary. If you get the roads wrong, everything turns into traffic.

This article covers how to do runtime composition in a modular monolith with enough rigor to survive enterprise reality: domain-driven design, progressive strangler migration, reconciliation, Kafka integration, operational constraints, and the uncomfortable tradeoffs that architects usually wave away with a box-and-arrow diagram.

Context

A modular monolith is not “a monolith with folders.” It is a single deployable system organized around cohesive business modules, with explicit boundaries, constrained dependencies, and a real model of the domain. In domain-driven design terms, the aspiration is clear: shape modules around bounded contexts or at least around stable subdomains, and prevent accidental coupling through APIs, internal contracts, and ownership rules.

That gives you one critical benefit: the architecture still has a chance to reflect the business.

But enterprises rarely remain still. Pricing differs by market. Order orchestration changes by channel. Risk policies vary by customer segment. Regulatory checks turn on and off by geography. Acquired products bring their own lifecycle rules. New channels need different process steps, not just different screens.

This variation shows up in one of three places:

  1. Domain rules vary
  2. Workflow steps vary
  3. Integration pathways vary

If your modular monolith cannot compose those variations at runtime, the differences leak into if-statements, inheritance trees, feature-flag chaos, or duplicated modules. None of those scale. They all eventually turn architecture into archaeology.

Runtime composition is how you keep one deployable unit while allowing the application to assemble the right domain behavior, process flow, and infrastructure participation for a given context: tenant, region, product line, channel, or migration stage.

The key is this: runtime composition must not erase the domain model. If it does, you have not built a flexible monolith. You have built a plugin container with invoices.

Problem

Most teams attack runtime variability in the wrong layer.

They start with technical extension points: strategy interfaces, dependency injection, plugin loading, rule engines, Spring profiles, toggles, custom scripting, perhaps a workflow engine. Those tools are useful. But if they are introduced before the domain boundaries are understood, they become a second architecture laid on top of the first. Soon nobody knows where business meaning lives.

A pricing override might sit in a plugin.

A fulfillment rule might be in a feature flag.

A fraud check might be split between Kafka consumers and domain services.

A product-specific process might be hard-coded in an “orchestrator” that now knows too much.

The result is a system that is configurable but not intelligible.

The deeper problem is that enterprises need runtime composition for several different reasons at once:

  • Customization: one tenant or business line needs different behavior.
  • Evolution: one module is being modernized or replaced.
  • Migration: some capabilities are still local; others now call services.
  • Integration: some flows are transactional; others are event-driven.
  • Reconciliation: async boundaries introduce drift that must be managed.

A modular monolith without runtime composition becomes rigid.

A modular monolith with poorly designed runtime composition becomes invisible.

Neither is acceptable.

Forces

Several forces pull against each other here.

1. Domain clarity vs flexibility

Domain-driven design tells us to make the model explicit. Runtime composition introduces indirection. Every extra indirection risks blurring domain semantics. If “Order Approval” can mean six different things depending on registry configuration, the domain language starts to rot.

2. Transactional simplicity vs asynchronous reality

The monolith’s great gift is local transactionality. Runtime composition often tempts teams to insert asynchronous messaging everywhere because it feels future-proof. But async introduces reconciliation, duplicate handling, reordering, and temporal inconsistency. Those are not implementation details; they are business facts.

3. Reuse vs bounded context integrity

A shared composition layer can reduce duplicate plumbing. It can also become a cross-context control plane that quietly standardizes things that should remain separate. Shared runtime infrastructure often becomes a smuggler of coupling.

4. Customization vs operability

Per-tenant or per-country composition sounds elegant until support teams discover that no two production environments behave the same way. Dynamic assembly buys flexibility at the cost of observability, supportability, and predictable testing.

5. Migration speed vs architectural debt

During strangler migrations, runtime composition is a powerful bridge. It allows a monolith to dispatch some capability locally and some remotely. But bridges have a way of becoming neighborhoods. Temporary composition points often become permanent scars.

These forces are why simplistic advice fails. “Just use plugins.” “Just split it into services.” “Just keep it in the monolith.” Those are bumper stickers, not architecture.

Solution

The most effective pattern is to treat runtime composition as a domain-aware assembly problem, not a generic extension problem.

The shape looks like this:

  • Stable domain modules own business concepts and invariants.
  • A composition layer selects and wires implementations based on runtime context.
  • Policies determine behavior variation.
  • Process coordinators assemble workflows across modules.
  • Integration adapters allow certain capabilities to be fulfilled either locally or externally.
  • Event publication and reconciliation handle asynchronous side effects where transaction boundaries cannot.

This works when composition happens at the right levels.

Composition Level 1: Policy selection

Use policy composition when the business concept is stable but rules vary.

For example:

  • discount eligibility by market
  • shipment routing by region
  • credit threshold by customer segment

The domain concept remains recognizable. You are not changing what an order is; you are selecting the policy that governs some decision.

Composition Level 2: Workflow assembly

Use workflow composition when the process steps differ by context.

For example:

  • B2B orders need contract validation and manual review
  • D2C orders go straight to payment authorization
  • regulated products require compliance screening before allocation

This is where a process coordinator or application service assembles the flow from reusable capabilities.

Composition Level 3: Fulfillment routing

Use fulfillment composition when the same capability may be provided by:

  • an internal module,
  • a local adapter,
  • or a remote service.

This is critical during migration. “Customer Credit Check” might be local today and a microservice tomorrow. The domain should not care more than necessary. microservices architecture diagrams

Composition Level 4: Event participation

Use event composition when post-transaction consequences vary:

  • publish to Kafka only for some channels
  • send downstream projections to specific consumers
  • reconcile external state when remote systems lag or fail

This is where many architectures become sloppy. Event participation is still part of the business operating model. Treat it as such.

Here is a representative composition model.

Composition Level 4: Event participation
Composition Level 4: Event participation

A few opinions, stated plainly.

First: do not let the composition layer own business decisions it cannot name in business language. If a class cannot explain itself without technical words like provider, resolver, injector, or handler, it probably sits too low in the stack.

Second: compose behavior around domain semantics, not infrastructure types. “Use Kafka” is not a business decision. “Emit OrderApproved because fulfillment is externalized and downstream inventory must react asynchronously” is.

Third: prefer explicit assembly over magical discovery. Reflection-heavy plugin architectures impress architects and terrify operators. A runtime system should be inspectable by ordinary engineers at 2 a.m.

Architecture

The architecture of runtime composition in a modular monolith is usually best organized into five layers of concern.

1. Domain modules

These encapsulate aggregates, domain services, invariants, value objects, and domain events. If you are using DDD properly, this is where ubiquitous language lives.

Examples:

  • Order Management
  • Pricing
  • Payment
  • Risk
  • Customer Account
  • Fulfillment

These modules should expose clear APIs. Internals remain hidden.

2. Context resolution

A runtime context resolver determines the relevant dimensions of variation:

  • tenant
  • region
  • product line
  • channel
  • regulatory classification
  • migration stage

This context must be explicit and testable. Do not hide it in thread-locals, global state, or implicit request metadata. Architecture gets brittle when context becomes folklore.

3. Composition registry

A registry maps runtime context to domain policy implementations, workflow templates, fulfillment routes, and event participation rules.

This can be code-based, config-backed, or metadata-driven. In enterprise systems, a mixed approach usually works best:

  • code for significant business behavior
  • config for safe selection and activation
  • database metadata only when governance is very strong

Why not fully database-driven? Because once core business composition is editable like a CMS, you have moved architecture into production data. That path is paved with emergency change windows.

4. Process coordinators

These are application-level orchestrators. They invoke modules in the right order and handle transactional boundaries. They should not become god objects. Their job is assembly, not business rule ownership.

A healthy coordinator sounds like:

  • validate order type
  • resolve pricing policy
  • request risk assessment
  • commit order state
  • publish domain event

An unhealthy coordinator sounds like:

  • calculate 47 rule variations inline because “it was easier”

5. Integration and reconciliation

When local and remote implementations coexist, the architecture must support:

  • routing
  • retries
  • idempotency
  • outbox publication
  • inbox consumption
  • reconciliation jobs
  • compensating actions where necessary

This is not optional. The minute Kafka or remote services enter the picture, failure stops being exceptional and becomes structural.

A useful conceptual architecture looks like this.

5. Integration and reconciliation
Integration and reconciliation

Notice the important detail: reconciliation comes back into the domain module, not into some technical “sync service.” The business state must be repaired in business terms.

Migration Strategy

Runtime composition earns its keep during migration.

A modular monolith is often the best starting point for a progressive strangler migration because the internal boundaries already define candidate seams. You are not carving services out of chaos. You are externalizing selected responsibilities from existing modules.

The pattern works like this:

Step 1: Strengthen module boundaries

Before extracting anything, make sure the module has:

  • a clean API
  • isolated persistence access as much as possible
  • no hidden calls into random shared utilities
  • explicit events
  • testable invariants

If the module is not internally coherent, do not extract it. You will just move confusion over the network.

Step 2: Introduce fulfillment routing

Replace direct calls to a candidate module with a routed capability interface. The composition layer can now decide:

  • local implementation
  • remote implementation
  • shadow mode to both

This is where a lot of value appears. The rest of the monolith can remain untouched.

Step 3: Add outbox and Kafka publication

For capabilities that need eventual consistency, publish domain events via an outbox. This ensures local commit and message publication remain coordinated without distributed transactions.

Step 4: Run in parallel

During strangler migration, it is often wise to run local and remote paths in parallel:

  • local remains source of truth
  • remote service receives events or duplicate requests
  • outputs are compared
  • discrepancies feed reconciliation and confidence reporting

This is not glamorous, but it is how serious enterprises migrate. Big systems are not switched over; they are proven over.

Step 5: Flip source of fulfillment

Once confidence is high, the composition registry routes the capability to the remote service. Some read models may still be local. Some events may still come back into the monolith. That is fine. Migration is not a single cut. It is a controlled redistribution of responsibility.

Step 6: Reconcile and retire

After cutover, expect mismatch:

  • duplicate messages
  • out-of-order events
  • transient remote failures
  • stale projections
  • partial acknowledgements

Build reconciliation as a first-class process. Eventually retire the local implementation only when exception paths are stable, not merely when happy-path demos succeed.

This migration shape is worth illustrating.

Step 6: Reconcile and retire
Reconcile and retire

The big idea is simple: runtime composition turns migration from a rewrite event into a routing decision.

Enterprise Example

Consider a global insurance platform handling policy issuance, underwriting, claims intake, and billing across nine countries.

The company began with a modular monolith. That was the right call. Policy, Billing, Claims, Customer, Documents, and Underwriting were separate modules. Deployment was simple. Reporting was centralized. Most transactions were local and reliable.

Then reality intruded.

Germany required additional compliance checks before policy issuance.

The UK needed a different pricing policy.

Claims in North America had to integrate with a third-party fraud platform.

Billing in two countries was being replaced by a SaaS platform.

Underwriting analytics was moving to a microservice environment because data science teams needed independent release cycles.

The first instinct from one architecture group was predictable: “split everything into microservices.” That would have been a disaster. The organization did not have the operational maturity. More importantly, not every variation justified a service boundary.

Instead, they introduced runtime composition in the monolith.

  • Policy module remained core and stable.
  • Underwriting policy selection became runtime-composed by region and product category.
  • Claims intake workflow used a process coordinator to add optional fraud screening and manual review stages.
  • Billing fulfillment was routed either to the local billing module or to the SaaS adapter based on country and migration phase.
  • Kafka-based event publication was added for underwriting and billing state changes.
  • Reconciliation workers compared local issuance state with remote billing confirmations.

This was not a theoretical win. It solved concrete enterprise problems.

A motor policy in Germany followed:

  1. collect application
  2. local pricing policy selection
  3. regional compliance screening
  4. underwriting risk evaluation
  5. issue policy locally
  6. publish PolicyIssued
  7. route billing to external SaaS
  8. wait for async billing account creation
  9. reconcile if acknowledgement missing after SLA threshold

A UK home insurance policy followed a different composition:

  1. collect application
  2. UK pricing policy selection
  3. streamlined underwriting
  4. issue policy
  5. local billing module handles account creation
  6. no SaaS reconciliation path needed

Same monolith. Same deployable artifact. Different runtime composition. Business semantics remained visible.

And later, when underwriting analytics moved into a microservice estate, the existing fulfillment routing and event model made the transition gradual rather than traumatic.

This is the sort of architecture enterprises actually need: one that respects uneven evolution.

Operational Considerations

Runtime composition raises the operational bar. Flexibility without observability is just a more expensive outage.

Observability

You need to know, for any transaction:

  • what context was resolved
  • which policy set was selected
  • which workflow variant was executed
  • whether fulfillment was local or remote
  • what events were emitted
  • what reconciliation actions were taken

This means structured logs, execution traces, and business-level audit trails. Not one of the three. All three.

Configuration governance

Composition rules must be governed like code even when stored as config:

  • version them
  • review changes
  • test them in representative environments
  • expose effective runtime configuration
  • support safe rollback

A composition bug is often worse than a code bug because it can alter system behavior without redeployment. That sounds agile until auditors arrive.

Idempotency

If Kafka or remote calls are involved, duplicate handling is mandatory. Every externally visible action should have a business idempotency key where possible.

Reconciliation

Reconciliation deserves more respect than it gets. In many enterprises, it is the hidden backbone of reliability. Once you have eventual consistency between local modules, Kafka consumers, and external services, there will be drift.

Drift happens because:

  • messages arrive late
  • retries duplicate actions
  • downstream services succeed after timeouts
  • upstream services roll back assumptions
  • operators manually intervene

Reconciliation should be designed as a domain capability:

  • detect missing confirmations
  • compare expected and actual state
  • repair projections
  • trigger compensations
  • surface human work queues where automation stops

Do not bury reconciliation in infrastructure. If a billing account is missing for an issued policy, that is a business exception, not a queue issue.

Testing strategy

You need several layers:

  • module tests for domain semantics
  • composition tests for context-to-behavior mapping
  • workflow tests for process variants
  • contract tests for remote adapters
  • replay tests for Kafka event sequences
  • reconciliation tests for drift scenarios

Most failures in composed systems occur in the seams, so test the seams.

Tradeoffs

There is no free architecture. Runtime composition in modular monoliths buys flexibility, but not cheaply.

Advantages

  • preserves single-deployment simplicity
  • supports real business variation
  • enables progressive strangler migration
  • keeps domain boundaries visible
  • avoids premature service extraction
  • allows selective Kafka and microservice integration

Costs

  • more indirection
  • harder debugging if observability is weak
  • larger testing matrix
  • stronger need for governance
  • risk of composition logic becoming a shadow architecture
  • reconciliation overhead for async paths

The hardest tradeoff is conceptual: you gain adaptability by accepting that not every path is purely transactional anymore. That is uncomfortable for teams who love the monolith precisely because it avoids distributed systems problems. Yet enterprise change eventually drags those problems in. Runtime composition lets you contain them instead of spreading them everywhere.

Failure Modes

Architects should talk more about failure modes. Systems do not collapse where the slides said they would.

1. The plugin trap

The composition framework becomes more important than the domain. Teams talk about handlers, providers, pipelines, extensions. Nobody talks about underwriting, claims, or billing anymore. The model has been replaced by machinery.

2. Context explosion

Every new requirement adds another axis:

tenant + region + brand + channel + product + feature pack + migration phase.

Soon no one can predict behavior for a given request. Runtime composition turns into runtime roulette.

3. God coordinators

Application services become giant orchestrators with dozens of branches and fallback paths. The domain modules become passive libraries while the real business logic leaks upward.

4. Async wishful thinking

Teams add Kafka because they plan to externalize capabilities someday. But they do not add outbox, idempotency, replay handling, or reconciliation. The first incident teaches them what “eventual consistency” really means.

5. Permanent temporary migration paths

Local and remote implementations coexist for years. Nobody retires the bridge. Support teams now debug both. Complexity doubles while ownership becomes muddy.

6. Hidden shared database assumptions

A module is declared “extractable,” but remote fulfillment still depends on tables or schemas only available in the monolith. The service boundary exists on a slide, not in reality.

If you see these patterns emerging, stop and simplify. Runtime composition should make change safer, not merely possible.

When Not To Use

This pattern is powerful, but not universal.

Do not use runtime composition in a modular monolith when:

The domain is small and stable

If the business process barely varies and the deployment model is straightforward, composition may be needless complexity. Simple code beats elegant indirection.

The module boundaries are still unclear

If the team cannot explain the bounded contexts, runtime composition will only amplify the confusion. First get the domain right. DDD before dispatch tables.

Variation is mostly presentational

If differences live in UI labels, forms, or report formats rather than domain behavior, solve them in the presentation layer, not in core runtime assembly.

You need hard isolation now

If independent scaling, fault isolation, data sovereignty, or team autonomy are immediate non-negotiables, a modular monolith with runtime composition may be only a short stop. Use it as a bridge, not a destination.

The organization lacks operational discipline

If there is no appetite for observability, config governance, contract testing, or reconciliation, keep things simpler. Dynamic architectures punish weak operations. EA governance checklist

In short: do not use runtime composition because it sounds sophisticated. Use it because your domain truly varies and your migration path benefits from controlled routing.

Several adjacent patterns often work with runtime composition.

Bounded Contexts

This is the anchor. Without bounded contexts, runtime composition degenerates into arbitrary switching.

Hexagonal Architecture

Useful for isolating domain logic from infrastructure adapters. Especially valuable when local and remote fulfillment coexist.

Strategy Pattern

Good for policy variation, but too small as a complete architectural answer.

Process Manager / Saga

Helpful when cross-module workflows span async boundaries, though in a modular monolith many flows can still remain simpler than full sagas.

Outbox Pattern

Essential for reliable event publication to Kafka.

Strangler Fig Pattern

The migration backbone. Runtime composition is one of the most practical ways to implement strangler routing inside a modular monolith.

Anti-Corruption Layer

Important when remote services or acquired systems speak a different model than the monolith’s domain.

Summary

The modular monolith remains one of the most underrated architectural forms in enterprise systems. Not because it is simple, though it often is. Not because it avoids microservices, though that can be healthy. It matters because it gives the business a coherent shape before the network starts pulling it apart.

But modularity alone is not enough. Enterprises need systems that can vary by market, tenant, channel, regulation, and migration stage. They need some capabilities to remain local, others to move out, and many to coexist for a while. They need Kafka in selected places, not everywhere. They need reconciliation because the world is asynchronous and impolite.

That is what runtime composition patterns provide.

Done well, they preserve domain semantics while enabling runtime variability.

Done badly, they create a hidden framework that nobody understands.

My advice is blunt:

  • start with bounded contexts
  • compose around business meaning
  • keep context explicit
  • route capabilities, not arbitrary code
  • use Kafka where async is real, not fashionable
  • build reconciliation as a domain concern
  • treat migration paths as temporary until proven otherwise
  • retire composition branches aggressively when the move is complete

A good modular monolith is not a compromise. It is a disciplined center of gravity. Runtime composition is how that center remains stable while the enterprise keeps changing around it.

And that, in the end, is the architect’s real job: not to predict the future, but to leave the system enough shape that it can survive one.

Frequently Asked Questions

What is enterprise architecture?

Enterprise architecture aligns strategy, business processes, applications, and technology in a coherent model. It enables impact analysis, portfolio rationalisation, governance, and transformation planning across the organisation.

How does ArchiMate support architecture practice?

ArchiMate provides a standard language connecting strategy, business operations, applications, and technology. It enables traceability from strategic goals through capabilities and services to infrastructure — making architecture decisions explicit and reviewable.

What tools support enterprise architecture modeling?

The main tools are Sparx Enterprise Architect (ArchiMate, UML, BPMN, SysML), Archi (free, ArchiMate-only), and BiZZdesign. Sparx EA is the most feature-rich, supporting concurrent repositories, automation, scripting, and Jira integration.