How To Share Code In Modular Monolith? Supporting Module Integration Pattern
The Supporting Module Pattern reduces duplication by extracting shared functionality into a dedicated module. It promotes modularity, decoupling, and team independence, but introduces trade-offs like potential coupling and complexity. Apply only with a clear need and ownership model.

The Supporting Module Pattern addresses the challenge of code duplication in modular monolith architectures by extracting common functionality into a dedicated supporting module. This pattern creates a shared module that provides cross-cutting concerns used by multiple feature modules, while maintaining clear boundaries and preventing tight coupling between core business modules.
A few rules are crucial to understand this article. You have to know that each pattern or approach is valid only in a specific context. You have to choose an approach suitable for your case, and it will never be ideal. It is always a trade-off.

When to Apply
This pattern is appropriate when:
- Multiple modules need common behavior that doesn't belong to any specific domain.
- Feature teams need to remain independent without tight coupling between their modules. That is why the team is considering extracting atomic functionality to a separate module.
- No single team wants ownership of the shared functionality within their module boundaries.
- Synchronization and consistency are required across modules for specific operations (assuming working in the same memory space).
- Cross-cutting concerns like logging, validation, or formatting are duplicated across modules.
- Technical infrastructure needs to be shared without being module-specific.

Trade-offs
Advantages:
- Reduces code duplication across feature modules.
- Promotes consistency in cross-cutting concerns and shared utilities.
- Enables independent team development while sharing common functionality.
- Simplifies maintenance of shared components in a single location.
- Improves system coherence by centralizing common patterns.
Disadvantages:
- Creates coupling between modules and the supporting module (potential risk).
- Can become a bottleneck if not properly designed or maintained.
- May accumulate unrelated functionality over time without clear boundaries.
- Requires careful change management to avoid breaking dependent modules.
- Requires quality tests. The more consumers there are, the more important this requirement becomes.
- Adds complexity in terms of versioning and deployment coordination.
When to Reconsider
Reconsider using this pattern when:
- Only one module actually uses the shared functionality.
- The shared functionality is evolving rapidly and changes frequently.
- The supporting module is becoming too large or unfocused.
- Team ownership of the supporting module is unclear or disputed.
- Deployment dependencies are causing significant friction between teams.
Implementation Guidelines
Supporting module could contain:
- Cross-cutting utilities (logging, validation, formatting).
- Common data transformations used by multiple domains (without any other specific domain details).
- Shared integration adapters (external API clients, messaging utilities).
- Technical infrastructure that isn't domain-specific.
- Instead of direct access to the Supporting Module functionalities, you could be use a separate public contract which implementation is implementation detail of Supporting Module (have a look at the diagram down below).

Access to Supporting Module by public contract
A natural consequence of the pattern shown in the diagram is loose coupling between the Supporting Module and its client modules. This is achieved by introducing a public contract (interface or abstraction layer) that prevents direct dependencies on the concrete Supporting Module implementation.
Rather than modules directly referencing the Supporting Module, they depend on the contract interface. An Inversion of Control (IoC) container manages the dependency resolution, binding the contract to the specific Supporting Module implementation at runtime. This architectural approach ensures that client modules remain decoupled from the implementation details while still benefiting from shared functionality.
This approach enables easier testing through contract mocking and substitution.
What Should Stay Out
At all costs, avoid placing these in supporting modules:
- Business logic specific to any domain.
- Domain entities or value objects.
- Complex workflows that might evolve independently.
- Features with unclear ownership or governance.
Ownership and Maintaining
Establish clear ownership of the supporting module:
- Designate which team maintains and evolves the supporting module.
- Define how breaking changes are communicated and managed (consider versioning of public contract).
- Team needs to be aware to not add specific module-bias changes to the supporting module.

The Upstream and Downstream concept is crucial to understand the proper way to decide where to put a potential shared logic.