Loading content...
When discussing object-oriented design, we frequently reference coupling (how dependent classes are on each other) and cohesion (how related the responsibilities within a class are). These concepts don't stop at the class level—they scale up to packages, modules, and entire subsystems.
At the package level, coupling and cohesion become even more critical because they determine:
Understanding package-level coupling and cohesion gives you metrics and mental models to evaluate whether your codebase structure is healthy or degenerating into an unmaintainable tangle.
By the end of this page, you'll understand package coupling and cohesion as measurable properties, learn Robert C. Martin's package design principles, and gain the ability to diagnose structural problems in codebases by reasoning about these metrics.
Package coupling measures how dependent one package is on other packages. When Package A imports classes, functions, or types from Package B, we say A is coupled to B, or A depends on B.
Unlike class-level coupling (which affects individual class maintenance), package coupling has systemic effects:
12345678910111213141516171819202122232425262728293031
Package: OrderService┌─────────────────────────────────────────────────────────────┐│ ││ Afferent Coupling (Ca = 5) ││ ───────────────────────────── ││ Who depends on me? ││ • CheckoutController → OrderService ││ • AdminDashboard → OrderService ││ • ReportingModule → OrderService ││ • NotificationService → OrderService ││ • MobileAPI → OrderService ││ ││ This package is heavily depended upon. ││ Changes to its public interface are HIGH RISK. ││ ││ Efferent Coupling (Ce = 3) ││ ───────────────────────────── ││ Who do I depend on? ││ • OrderService → UserRepository ││ • OrderService → PaymentGateway ││ • OrderService → InventoryService ││ ││ This package has moderate external dependencies. ││ Changes in these packages may affect me. ││ ││ Instability I = 3 / (5 + 3) = 0.375 ││ ───────────────────────────── ││ This package is moderately stable. ││ It has more responsibility than freedom to change. ││ │└─────────────────────────────────────────────────────────────┘Packages should depend in the direction of stability. If Package A (unstable, I=0.8) depends on Package B (stable, I=0.2), changes to B force changes to A. Since A is unstable, that's acceptable—A is expected to change. But if B depended on A, B would be forced to change whenever the volatile A changes, violating its stable nature.
Rule: Stable packages → depended upon. Unstable packages → depend on others.
Package cohesion measures how related the classes within a package are to each other. High cohesion means all classes in the package work toward a common purpose and are likely to change for the same reasons. Low cohesion means the package is a grab-bag of unrelated classes—a dumping ground that resists comprehension.
Robert C. Martin identified three principles of package cohesion, each addressing a different concern:
1. The Reuse-Release Equivalence Principle (REP)
The granule of reuse is the granule of release.
If classes are packaged together for reuse, they should be released together. Either all classes in a package are reusable together, or none are. This means packages shouldn't contain a mix of reusable utilities and application-specific code.
2. The Common Closure Principle (CCP)
Classes that change together belong together.
A package should contain classes that are closed together against the same kinds of changes. When a requirement changes, ideally only one package needs to be modified. CCP is the package-level equivalent of the Single Responsibility Principle.
3. The Common Reuse Principle (CRP)
Don't force users of a package to depend on things they don't need.
Classes in a package should be used together. If a client uses one class in a package, it should use most of them. Conversely, if you only use 10% of a package's classes, that package has low cohesion from your perspective, and you're carrying unnecessary transitive dependencies.
| Principle | Key Question | Violation Symptom | Resolution Strategy |
|---|---|---|---|
| REP (Reuse-Release) | Can all classes be released together? | Some classes need different versioning or release cycles | Split into separately releasable packages |
| CCP (Common Closure) | Do classes change for the same reason? | Requirements change touch multiple packages | Regroup classes by reason for change |
| CRP (Common Reuse) | Are all classes used together? | Clients import a package but only use a fraction | Split package to reduce unnecessary coupling |
These three principles are in tension. CCP pushes toward larger packages (group everything that changes together), while CRP pushes toward smaller packages (don't include things clients don't need). REP pushes toward packages aligned with release/versioning boundaries.
There's no perfect balance. Early in development, lean toward CCP (group by change reason) to reduce coordination overhead. As the system stabilizes the component interfaces, lean toward CRP (minimize unnecessary dependencies) to reduce coupling.
While coupling and cohesion can be reasoned about qualitatively, there are tools and metrics that provide quantitative insight into your package structure's health.
1234567891011121314151617181920212223242526272829
Abstractness (A)│1.0 ┤ ○ Zone of Uselessness │ (Abstract but nobody depends on it) │ ╲ │ ╲ Main Sequence │ ╲ (A + I = 1)0.5 ┤ ╲ ●●●● (Healthy packages) │ ╲ │ ╲ │ ╲ │ ╲0.0 ┼────────────○─────────────── Instability (I) 0.0 0.5 1.0 Zone of Pain (Concrete but stable - hard to change, painful to extend) Legend:○ = Problematic packages (avoid these zones)● = Healthy packages (along the main sequence) Abstractness (A) = Abstract classes/interfaces ÷ Total classesInstability (I) = Ce ÷ (Ca + Ce) Ideal: Packages fall on or near the main sequence line.- Highly abstract packages should be stable (low I)- Highly unstable packages can be concrete (low A)The Zone of Pain:
Packages in the Zone of Pain (high stability, low abstractness) are concrete and heavily depended upon. They contain implementation details that are hard to change without breaking many clients. Examples include concrete utility classes that became de facto standards, or database access code that everything depends on directly.
The Zone of Uselessness:
Packages in the Zone of Uselessness (high abstractness, high instability) contain abstractions that nobody uses. They were over-engineered, and the expected clients never materialized. These packages add cognitive overhead without providing value.
The Main Sequence:
Healthy packages follow the main sequence: maximal abstractness with stability (everyone depends on interfaces) or maximal concreteness with instability (implementation details that can change freely).
One of the most insidious package structure problems is circular dependencies (also called cyclic dependencies). A circular dependency exists when Package A depends on Package B, which depends on Package C, which depends back on Package A.
A → B
↑ ↓
C ←─┘
Circular dependencies create a tangled mess that undermines the entire purpose of modularization.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556
// ❌ BEFORE: Circular dependency// order-service/OrderService.tsimport { UserService } from '../user-service/UserService';export class OrderService { constructor(private userService: UserService) {} createOrder(userId: string) { const user = this.userService.getUser(userId); // ...create order }} // user-service/UserService.tsimport { OrderService } from '../order-service/OrderService'; // CYCLE!export class UserService { constructor(private orderService: OrderService) {} getUserOrders(userId: string) { return this.orderService.getOrdersForUser(userId); }} // ✅ AFTER: Broken with Dependency Inversion// user-service/interfaces/OrderProvider.ts (interface in user-service)export interface OrderProvider { getOrdersForUser(userId: string): Order[];} // user-service/UserService.ts (depends only on interface)import { OrderProvider } from './interfaces/OrderProvider';export class UserService { constructor(private orderProvider: OrderProvider) {} getUserOrders(userId: string) { return this.orderProvider.getOrdersForUser(userId); }} // order-service/OrderService.ts (implements the interface)import { OrderProvider } from '../user-service/interfaces/OrderProvider';import { UserService } from '../user-service/UserService'; export class OrderService implements OrderProvider { constructor(private userService: UserService) {} createOrder(userId: string) { const user = this.userService.getUser(userId); // ... } getOrdersForUser(userId: string): Order[] { // ... }} // Now: UserService → OrderProvider (interface)// OrderService → UserService// OrderService implements OrderProvider// No cycle! UserService doesn't know about OrderService.Low-cohesion packages are endemic in enterprise codebases. Recognizing these anti-patterns helps you identify structural problems before they calcify.
utils are rarely cohesive—StringUtils has nothing in common with DateFormatter or HttpHelper. Split by domain: text-processing, date-handling, http-clientInterestingly, there's also 'too cohesive'—packages so narrowly focused that you have hundreds of them, each with one or two classes. This creates a different kind of navigation nightmare. The goal is meaningful groupings that align with either business concepts or technical layers, not maximum granularity.
A package should be 'about something'—a topic you could explain in one sentence.
The Symptom Test:
Ask yourself: 'If I need to add a feature to this package, what might I add?' If the answer is 'anything,' the package lacks cohesion. A cohesive user-authentication package might grow with MfaService, PasswordPolicy, or LoginAttemptTracker—all related to authentication. A utils package could grow with literally anything, revealing its lack of identity.
The Acyclic Dependencies Principle (ADP) mandates that the dependency graph of packages must contain no cycles. This is not merely a guideline—it's a structural requirement for maintainable systems.
"The dependency structure of packages must be a Directed Acyclic Graph (DAG). There must be no cycles."
— Robert C. Martin
123456789101112131415161718192021222324252627282930313233343536373839
// architecture.test.tsimport { buildDependencyGraph, findCycles } from './arch-testing-utils'; describe('Package Architecture', () => { it('should have no circular dependencies between packages', () => { const graph = buildDependencyGraph('./src'); const cycles = findCycles(graph); if (cycles.length > 0) { const cycleDescriptions = cycles.map(cycle => cycle.join(' → ') + ' → ' + cycle[0] ); fail( 'Circular dependencies detected:' + cycleDescriptions.join('') + ' Use Dependency Inversion or extract shared packages to break these cycles.' ); } expect(cycles).toHaveLength(0); }); it('should follow layer dependency rules', () => { const violations = checkLayerViolations('./src', { layers: [ { name: 'domain', pattern: '**/domain/**' }, { name: 'application', pattern: '**/application/**', allowedDependencies: ['domain'] }, { name: 'infrastructure', pattern: '**/infrastructure/**', allowedDependencies: ['domain', 'application'] }, ] }); expect(violations).toHaveLength(0); });});It's vastly easier to prevent cycles than to break them later. Once a cycle exists, code tends to grow within the cycle, making it increasingly painful to break. Automate cycle detection in CI/CD pipelines and treat cycle introduction as a build failure.
Beyond breaking cycles, there are strategic approaches to reduce coupling and improve the independence of your packages.
12345678910111213141516171819
// order-processing/index.ts// ❌ BAD: Export everything, large coupling surfaceexport * from './OrderService';export * from './OrderRepository';export * from './OrderValidator';export * from './Order';export * from './OrderItem';export * from './OrderStatus';export * from './internal/OrderCalculator';export * from './internal/TaxService'; // ✅ GOOD: Explicit public API, minimal surfaceexport { OrderService } from './OrderService';export { Order, OrderStatus } from './Order';export type { OrderId, CreateOrderRequest } from './Order'; // Everything else is internal - not exported// Clients can only couple to OrderService and Order types// Internal changes (OrderCalculator, TaxService) don't affect clientsRemember that coupling isn't just direct. If Package A depends on Package B, and B depends on Package C, A is transitively coupled to C. This is why heavy dependencies (like ORMs) should be confined to specific packages—their transitive closure pollutes anything that depends on them.
Let's consolidate the key principles for healthy package design:
What's next:
Understanding coupling and cohesion gives us the metrics to evaluate package health. In the next page, we'll explore module boundaries—how to define where one module ends and another begins, and the strategic decisions involved in drawing those lines.
You now understand package coupling and cohesion as measurable, actionable properties. You can identify circular dependencies, apply breaking strategies, and recognize anti-patterns that harm package quality. Next, we'll explore how to define and enforce module boundaries.