How To Share Code In Modular Monolith? The Module Contract Pattern Overview.

Module Contract Pattern is a pattern that enables sharing business logic between modules in a modular monolith through an explicit, stable contract that is implemented and registered in an IoC container, ensuring consistent yet isolated collaboration.

How To Share Code In Modular Monolith? The Module Contract Pattern Overview.

The Contract Module pattern addresses the common scenario where multiple modules (domains) within a modular monolith need to access the same business logic or data structures. Instead of duplicating code across features, this pattern assumes an existing well-defined public contract which is related to some module. Other modules can use the contract to be able to receive expected data or behavior. Thanks to the IoC container, the contract is registered with a specific implementation.

🧱
This is one of the few overview articles about modular monolith architecture and integration patterns between modules. The rest can be found here.

In this pattern, common business logic is encapsulated in a contract module that exposes only the necessary types and interfaces through a public API, while keeping internal implementation details private. This allows modules (features) to collaborate without direct dependencies on each other's internal structures.

Module B (Consumer) uses a clean contract without implementation (Provider) details.

The main magic is done by the IoC container. In the root you need to connect the module and specific contract. Other modules use the IoC container to get an instance of the integration.

Public contract uses a dedicated public language. This means it has dedicated types to be used for integration purposes.

When to Apply This Pattern

This pattern is appropriate when:

  • More than one module require identical business logic that would otherwise be duplicated.
  • Teams have explicit awareness of the integration requirements and associated coupling risks
  • Consumer teams lack capacity to implement and maintain their own versions of the functionality. So they decide to integrate with another module which provides it.
  • The shared logic is stable and unlikely to change frequently based on individual feature requirements..
  • Time-to-market pressures make code reuse more valuable than perfect isolation.
  • Clear business justification exists for the coupling (e.g., shared navigation, authentication, or logging services).

Trade-offs

Advantages:

  • Reduced code duplication across features and domains.
  • Faster development for consumer features that can leverage existing implementations.
  • Implementation hiding. Internal module (Provider) changes don't affect consumers as long as the contract remains stable.
  • Centralized maintenance of common business logic and rules.
  • Consistent behavior across features using the shared functionality.
  • Lower cognitive load for developers who don't need to understand other modules (domain) background.
  • Explicit dependencies: Makes cross-module relationships visible and intentional.
  • Supports the development of modules (functionalities) by multiple teams and enabling stable integration between them.
1️⃣
The context is important.
2️⃣
No ideal solution, only trade-offs.
3️⃣
Architecture changes over time and evolves.

Disadvantages (or negative consequences):

  • Introduces coupling between otherwise independent features. It is still relatively low, since a public language in contract is applied.
  • Requires discipline to maintain contract stability and backward compatibility (versioning of public stuff).
  • Increases the focus on the quality and stability of the implementation by the team that has many consumers.
  • Potential bottleneck: The providing team becomes responsible for supporting multiple consumers.
  • Team could be not familiar with the approach.
🧩
Public Language in Contracts.

The term public language refers to a dedicated integration language that is explicitly designed for communication between modules. It is inspired by the concept of the Bounded Context and Ubiquitous Language from Domain-Driven Design (DDD).

When to Reconsider

You should reconsider or modify this pattern when:

  • Contract changes frequently: Indicates unstable module boundaries or poor public interface design.
  • The organization grows and team autonomy becomes more important than code reuse.
  • The functionality could be standardized across the organization and extracted to a shared library

Implementation Guidelines

  • Expose only what's absolutely necessary, without low-level implementation details. Expose behavior.
  • Use a public language (dedicated types) in the contract project.
  • Version your public contracts to manage breaking changes.
  • Think about the contract project in terms of REST API.
  • Implement comprehensive testing, including integration tests with consumer features
  • Contract project has no access to related module. Check diagram.
  • Document dependencies clearly and maintain architectural decision records.
  • Use extended comments for contract methods, functions, and types. Think about them as a public library.
  • Do not expose internal components to be used in other modules. This approach introduces high coupling. Implement public contract.
  • Use dependency injection to register implementation for contract.
  • Public contract can only be changed by the team responsible for the module.
  • Create communication channels for coordinating changes and requests from other teams.

The Module Contract pattern can be valuable for reducing duplication and accelerating development, but it requires careful consideration of the coupling it introduces and ongoing coordination between teams to remain effective.