Design Tactics to
Reduce Coupling
Patterns and strategies to reduce structural, instantiation, and call dependencies β and a comparison of static vs. dynamic coupling trade-offs.
Design Tactics to Reduce Coupling in Structural Dependencies
A Structural Dependency exists when a component is directly reliant on the structure or design of another component β changes to one can ripple and break the other.
Polymorphism
- Program to interfaces β depend on abstractions, not concrete types
Decorator Pattern
- Wrap & extend behaviour without modifying original classes
Favour Composition
- Compose objects at runtime instead of rigid inheritance hierarchies
- Subclass is directly coupled to the structure of the superclass
- Change to a constructor name or method in the parent directly affects all subclasses
- Highest form of structural coupling β tight coupling
- A component uses a concrete type of another component directly
- Any change to the concrete class (constructor, method name) affects all consumers
- Can be mitigated using abstractions (interfaces)
Instead of depending on a concrete type, depend on an abstraction (interface). Multiple implementations can be swapped without the caller knowing.
NotificationService directly references the concrete EmailNotification class β switching to SMS or push requires modifying the service.Notification interface. EmailNotification, SmsNotification, etc. implement it β the service never changes.Introduce a Facade between your application and a complex third-party library. The application depends only on the Facade's clean interface β the underlying complexity is hidden and swappable.
WebAppdirectly depends onCredentials,ConfigManager, andDataStorage- Changing the library forces changes everywhere in the app
- Every consumer must understand internal library details
WebAppdepends only onStorageFacade- Facade hides third-party complexity behind a clean interface
- Swap the underlying library without touching the app
Instead of adding features directly to a class via inheritance, wrap the class with a Decorator that implements the same interface. Features are composed, not inherited.
CoreRequestHandler needs optional features. Adding each via inheritance creates tight coupling and an unmanageable subclass explosion.IHttpRequestHandler (interface) β CoreRequestHandler + RequestHandlerDecorator (abstract) β concrete decorators composed at runtime.RateLimitingDecorator) requires zero changes to existing classes.- Structural dependencies (inheritance and type usage) create tight coupling
- Two types: Inheritance (tightest) & Type Usage (concrete type dependency)
- Polymorphism β program to interfaces, not concrete types
- FaΓ§ade Pattern β hide third-party complexity behind a simple interface
- Decorator Pattern β compose behaviour without inheritance (no subclass explosion)
- Chain decorators for flexible, runtime feature composition
Design Tactics to Reduce Coupling in Instantiation Dependencies
An Instantiation Dependency (Creation Coupling) occurs when a component directly creates an instance of another component using new β making it tightly bound to the concrete implementation.
Factory Pattern
- Centralise object creation β consumers don't know the concrete type
Proxy Pattern
- Lazy initialisation β create objects only when actually needed
Dependency Injection
- Dependencies injected from outside by a DI container / assembler
- Centralise object creation in a factory class
- Consumer asks factory for an object β doesn't know the concrete type
- Adding new types = change factory only
- Lazy initialisation β create object only when needed
- Proxy sits between client and real object
- Enables permission/auth checks without touching real class
- Dependencies are injected from outside, not created internally
- Component doesn't decide which implementation to use
- Wiring done by a DI container / assembler
PolicySalesAgent directly instantiates policy classes. Adding HealthInsurancePolicy requires modifying PolicySalesAgent β tight coupling.PolicySalesAgent delegates creation to InsurancePolicyFactory. Adding new policy types only changes the factory β agent never changes.StudentAccountManager eagerly β even when not needed. Can't modify a third-party class to add lazy init or auth checks.StudentAccountManagerProxy wraps the real class, controls lazy init, and can add auth checks without touching the real class or any client.OrderService uses new to create concrete implementations β tightly coupled, untestable, and impossible to swap without changing the class.OrderService depends only on interfaces.- Instantiation coupling occurs when components create their own dependencies using
new - Factory Pattern β centralise object creation; adding new types only changes the factory
- Proxy Pattern β lazy initialisation & cross-cutting concerns without touching the real class
- Dependency Injection β inject dependencies from outside; component doesn't decide implementation
- DI container wires concrete implementations at startup
Design Tactics to Reduce Coupling in Call Dependencies
A Call Dependency exists when one component directly calls methods or network endpoints of another. This includes both in-process method calls and cross-process network calls (same machine or different machines).
Abstractions
- Depend on interfaces, not concrete implementations
Adapter Pattern
- Bridge incompatible interfaces without structural coupling
No Cycles
- Eliminate circular AβBβA dependencies with layering
Async Messaging
- Replace direct calls with broker-based messages
Restrict Communication
- Enforce rules β only allowed connections via APIs & gateways
Depend on interfaces, not concrete implementations. Callers are decoupled from who implements the contract.
Bridge incompatible interfaces. Allows a component to call another without direct structural coupling.
Circular call dependencies (A→B→A) create unresolvable coupling. Use architecture layers to prevent them.
Replace direct calls with messages via a broker. Sender decoupled from receiver β doesn't even need to know the receiver exists.
Enforce communication rules (layers, APIs, gateways). Only necessary connections allowed β prevents spaghetti coupling.
OrderService depends directly on CreditCardProcessor. Swapping payment providers requires changing the caller β tight call coupling.PaymentProcessor interface. OrderService depends only on the interface β any implementation can be swapped without touching the caller.OrderService calls processPayment(amount) but the bank provides BankAccountProcessor with debitAccount(amount, accountId) β incompatible signatures, can't plug in directly.BankPaymentAdapter implements PaymentProcessor and wraps BankAccountProcessor, translating processPayment(amt) β debitAccount(amt, id). Neither side changes.- A calls B, B calls C, C calls A β circular loop
- Cannot build or deploy components independently
- Changes ripple in all directions β unpredictable
- Testing becomes impossible without the full cycle
- Enforce one-directional call flow (top → bottom layers)
- Presentation → Business Logic → Data Access → Database
- No lower layer ever calls an upper layer
- Use dependency inversion if a lower layer needs to notify an upper one
- Sender doesn't expect data back → less dependency → less coupling
- Sender uses the broker interface, not the receiver's interface
- Receiver can be replaced, scaled, or taken offline independently
- Caveat: Cannot convert every call dependency to messaging β use where the result is not immediately needed
- Any service can call any other service directly
- Creates a spaghetti architecture β tangled, unmaintainable
- A change anywhere can break anything else
- No clear ownership or contract boundaries
- Enforce API Gateway / layer boundaries
- Services only call components they are architecturally permitted to
- Reduces accidental coupling from ad-hoc shortcuts
- Cleaner, auditable, controllable communication graph
- Call dependencies are the most common form of coupling β method calls & network calls
- Abstractions (interfaces) β decouple callers from concrete callees
- Adapter Pattern β bridge incompatible interfaces without changing either side
- No cyclical relationships β enforce one-directional call flow (layered architecture)
- Async Messaging β broker decouples sender & receiver; sender doesn't need to know the receiver
- Restrict Communication β enforce architecture rules via API gateways and layer boundaries
Static vs Dynamic Coupling
Important caveat: Moving from static coupling to dynamic coupling does NOT necessarily reduce coupling β it only changes when dependencies are resolved.
Static Coupling
- Dependencies resolved at compile time β visible in source code & type system
Dynamic Coupling
- Dependencies resolved at runtime β via DI, reflection, or configuration
- Explicit, fixed, or hard-coded dependencies
- Determined at compile time β without running the application
- Describes how components are "wired" to each other
- Example:
new CreditCardService()insideOrderServiceconstructor
- Dependencies are resolved at runtime
- Describes how components "call each other" during execution
- Example: DI injection β only at runtime do we know which implementation is used
- Example: Service Discovery β DNS resolves address at runtime
| Dimension | Static Coupling | Dynamic Coupling |
|---|---|---|
| Resolution Time | Compile time | Runtime |
| Performance | ✓ Less overhead β no reflection, DNS, or config parsing | ✗ Higher overhead β reflection, DNS resolution, config parsing |
| Tooling Support | ✓ Excellent β IDE autocompletion, refactoring, static analysis, compile-time errors | ✗ Limited β IDE can't trace dynamic lookups; no compiler errors |
| Readability | ✓ Easier to read, reason about, and maintain | ✗ Harder to trace β debugging and troubleshooting are complex |
| Flexibility | ✗ Rigid β modifying dependencies requires rebuild & redeploy | ✓ Flexible β modify dependencies without rebuild; move faster with new features |
| Extensibility | ✗ Harder to extend β changing a dep means redeploying all consumers | ✓ Greater potential β swap implementations via config, DI, or discovery |
| Runtime Risk | ✓ Lower β issues caught at compile time | ✗ Higher risk of runtime failures β missing dependencies, misconfiguration |
| Debugging | ✓ Easy β IDE, compiler & static analysis tools fully supported | ✗ Difficult β hard to analyse dependencies; restricted production access |
Example: Injecting
PayPalService into OrderService via a DI assembler makes the dependency dynamic β but OrderService is still coupled to PayPalService. The coupling exists; it's just resolved at a different time.Similarly, splitting a monolith into microservices without careful design produces: Low Cohesion → Tight Coupling β now with no static analysis and no compile-time error detection.
- Static and dynamic coupling are different trade-off profiles, not good vs. bad
- Static coupling = dependencies wired at compile time β better tooling, performance, readability
- Dynamic coupling = dependencies resolved at runtime β flexibility, extensibility
- Dynamic coupling risks: higher runtime risk, harder debugging, limited static analysis
- Dynamic β less coupling β it's just resolved at a different time
- Monolith β microservices β automatic decoupling β poor splits produce tight coupling without compile-time safety
- Rule of thumb: use static by default; dynamic only where runtime flexibility genuinely adds value
Structural Dependencies
- Inheritance β tightest coupling
- Type usage β concrete type dependency
- Polymorphism β program to interfaces
- FaΓ§ade β hide third-party complexity
- Decorator β compose over inherit
Instantiation Dependencies
- Creation coupling β using new directly
- Factory Pattern β centralised creation
- Proxy Pattern β lazy init & cross-cutting
- Dependency Injection β inject from outside
- DI container wires at startup
Call Dependencies
- Method calls & network calls
- Abstractions (interfaces)
- Adapter Pattern
- Avoid cyclical relationships
- Async Messaging via broker
- Restrict Communication
Static vs Dynamic Coupling
- Static = compile-time wiring
- Dynamic = runtime resolution
- Static: tooling, performance, clarity
- Dynamic: flexibility, extensibility
- Dynamic ≠ less coupling