Shared Contracts Are Useful. Shared Everything Is Not.
Shared code initially feels like the pinnacle of engineering efficiency: define a concept once, reuse it across multiple repositories or services, and eliminate duplication.
However, that approach quickly deteriorates when the shared package shifts from defining communication contracts to hosting internal implementation behavior.
What Belongs in a Shared Package
A healthy shared layer focuses exclusively on defining boundary interactions:
- API and event schemas: The explicit data payloads passed over HTTP, gRPC, or message brokers.
- Serialization formats: Protobuf definitions, JSON Schemas, or OpenAPI specifications.
- Domain identifiers: Universal value types such as tenant IDs, user IDs, or UUID wrappers.
- Boundary validation rules: Input constraints that validate whether an incoming payload is structurally sound.
- Generated client SDKs: Declarative clients that encapsulate communication mechanics without enforcing internal business policies.
These components define how systems communicate across boundaries.
The Trap of Shared Behavior
Architectural problems begin when the shared library expands to encapsulate internal mechanics:
- Database entities and ORM mappings
- Domain business rules and validation logic
- Service-specific persistence helpers
- Framework wrappers and shared infrastructure configuration
When this happens, services that appear isolated on architectural diagrams become tightly coupled through code. Modifying an internal database model forces version upgrades across multiple downstream services. Refactoring a shared helper creates coordinated deployment lockstep. Adding a dependency pulls third-party libraries into services that do not need them.
You succeeded in reducing code duplication, but you drastically increased architectural coupling.
Sharing Boundaries vs. Sharing Models
Consider two services interacting through an OrderCreated domain event.
Sharing the event schema makes complete sense: publishing fields like order_id, customer_id, and created_at establishes a clear public contract. Sharing the internal Order domain model, however, is a dangerous anti-pattern. The producing service may track complex payment states, inventory reservations, and fulfillment flags. The consuming notification service only needs the recipient ID and invoice amount.
Coupling both services to a monolithic internal model destroys each service's ability to evolve independently.
Share the Boundary, Not the Implementation
A fundamental architectural guideline for distributed systems is:
Share the boundary, not the implementation.
Contracts should be small, explicit, versionable, and slow to change. Implementation details should remain strictly local and privately owned. A small amount of intentional duplication between independent services is vastly preferable to an over-generalized shared library that binds disparate systems into an inflexible monolith.
The goal of shared code is never maximum reuse—it is minimum ambiguity at the boundary without sacrificing autonomy behind it.