LearningTree

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.

01
Chapter One Β· Reduce Coupling

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
Component A Component B Structural Dependency Required Changes Changes
Two Types of Structural Dependencies
Type 1 β€” Inheritance
  • 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
Animal Β«classΒ» + Animal(name: String) ⚠ rename constructor β†’ breaks ALL subclasses Dog Β«extends AnimalΒ» + Dog() super("Dog") ← coupled extends
Type 2 β€” Type Usage
  • 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)
OrderService Β«classΒ» - service: CreditCardService ⚠ hardcoded concrete β†’ tightly coupled CreditCardService Β«classΒ» + CreditCardService() + processPayment() uses
Tactics to Handle Coupling in Structural Dependencies
1
Polymorphism β€” Program to an Interface

Instead of depending on a concrete type, depend on an abstraction (interface). Multiple implementations can be swapped without the caller knowing.

❌ Tight β€” concrete type dependency
Problem: NotificationService directly references the concrete EmailNotification class β€” switching to SMS or push requires modifying the service.
NotificationService Β«classΒ» - notifier: EmailNotification + send(message) EmailNotification Β«classΒ» + send(message) depends on Tied to one concrete implementation
✓ Loose β€” interface dependency
Solution: Depend on a Notification interface. EmailNotification, SmsNotification, etc. implement it β€” the service never changes.
Notification Β«interfaceΒ» + send(message) EmailNotification + send(message) SmsNotification + send(message) NotificationService - notifier: Notification depends on abstraction
2
FaΓ§ade Pattern β€” Hide Complexity Behind a Simple Interface

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.

❌ Without Facade
Problem:
  • WebApp directly depends on Credentials, ConfigManager, and DataStorage
  • Changing the library forces changes everywhere in the app
  • Every consumer must understand internal library details
WebApp + run() Credentials + getToken() ConfigManager + load() DataStorage + save(), get() ⚠ Changing library = massive refactor everywhere
✓ With StorageFacade
Solution:
  • WebApp depends only on StorageFacade
  • Facade hides third-party complexity behind a clean interface
  • Swap the underlying library without touching the app
WebApp + run() StorageFacade + save(), get() Credentials ConfigManager DataStorage depends on hidden behind facade
3
Decorator Pattern β€” Add Behaviour Without Inheritance

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.

❌ Problem β€” Inheritance subclass explosion
Problem: CoreRequestHandler needs optional features. Adding each via inheritance creates tight coupling and an unmanageable subclass explosion.
CoreRequestHandler + handle(req) Auth+Core extends Core Cache+Core extends Core Compress+Core extends Core Auth+Cache+Core Auth+Compress+Core … ⚠ N features = 2α΄Ί subclasses β€” unmanageable
✓ Solution β€” Decorator Chain
Solution: IHttpRequestHandler (interface) β†’ CoreRequestHandler + RequestHandlerDecorator (abstract) β†’ concrete decorators composed at runtime.
IHttpRequestHandler Β«interfaceΒ» + handle(req): HttpResponse CoreRequestHandler + handle(req) RequestHandlerDecorator Β«abstractΒ» - wrapped: IHttpRequestHandler Authentication Decorator Caching Decorator Compression Decorator βœ“ Compose any combination at runtime β€” no subclass explosion
Decorator Implementations
Authentication Decorator
public class AuthenticationDecorator extends RequestHandlerDecorator { public HttpResponse handle( HttpRequest req) { if (!authenticate(req)) { return new HttpResponse(401, "Unauthorized"); } return wrappedHandler.handle(req); } }
Caching Decorator
public class CachingDecorator extends RequestHandlerDecorator { public HttpResponse handle( HttpRequest req) { HttpResponse cached = getCachedResponse(req); if (cached != null) return cached; HttpResponse res = wrappedHandler.handle(req); cacheResponse(req, res); return res; } }
Compression Decorator
public class CompressionDecorator extends RequestHandlerDecorator { public HttpResponse handle( HttpRequest req) { HttpResponse res = wrappedHandler.handle(req); return compressResponse(res); } }
Composing Decorators at Runtime
Component A β€” Basic (no extras)
handler = new CoreHttpHandler(); // No decorators β€” minimal
Component B β€” Auth + Compression
handler = new AuthenticationDecorator( new CompressionDecorator( new CoreHttpHandler() ) );
Component C β€” Cache + Compression
handler = new CompressionDecorator( new CachingDecorator( new CoreHttpHandler() ) );
Benefit: Each component composes only the features it needs β€” no inheritance coupling, no feature explosion. Adding a new decorator (e.g. RateLimitingDecorator) requires zero changes to existing classes.
📋 Chapter 1 — Summary
  • 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
02
Chapter Two Β· Reduce Coupling

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
Three Tactics for Instantiation Dependencies
1. Factory Pattern
  • 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
2. Proxy Pattern
  • Lazy initialisation β€” create object only when needed
  • Proxy sits between client and real object
  • Enables permission/auth checks without touching real class
3. Dependency Injection
  • Dependencies are injected from outside, not created internally
  • Component doesn't decide which implementation to use
  • Wiring done by a DI container / assembler
Tactic 1 β€” Factory Pattern
❌ Without Factory β€” grows with every new type
Problem: PolicySalesAgent directly instantiates policy classes. Adding HealthInsurancePolicy requires modifying PolicySalesAgent β€” tight coupling.
PolicySalesAgent new HomeInsurancePolicy(c,d) new CarInsurancePolicy(c,d) HomeInsurance Policy CarInsurance Policy HealthInsurance Policy ⚠ Every new type requires modifying PolicySalesAgent Add HealthInsurancePolicy β†’ must edit agent again
✓ With Factory β€” consumer decoupled from creation
Solution: PolicySalesAgent delegates creation to InsurancePolicyFactory. Adding new policy types only changes the factory β€” agent never changes.
PolicySalesAgent + sellPolicy(c, d) InsurancePolicyFactory + createPolicy(type) InsurancePolicy Β«interfaceΒ» HomeInsurancePolicy CarInsurancePolicy HealthInsurancePolicy βœ“ Add new policy type β†’ only factory changes
Tactic 2 β€” Proxy Pattern (Lazy Initialisation)
❌ Without Proxy β€” eager creation everywhere
Problem: All clients create StudentAccountManager eagerly β€” even when not needed. Can't modify a third-party class to add lazy init or auth checks.
EnrollmentService + enrollStudent(s) GradeService + assignGrade(s) StudentAccountManager + createAccount() Β«third-party β€” no modifyΒ» ⚠ Duplicate init logic Β· can't add auth to third-party class All clients coupled directly to concrete class
✓ With Proxy β€” lazy init & cross-cutting concerns
Solution: A StudentAccountManagerProxy wraps the real class, controls lazy init, and can add auth checks without touching the real class or any client.
AccountManager Β«interfaceΒ» + createAccount(s: Student) EnrollmentSvc GradeService StudentAcctMgrProxy + createAccount(s: Student) lazy init + auth checks StudentAcct Manager (real) ⚠ Duplicate init logic Β· can't add auth to third-party class βœ“ Clients depend on interface Β· proxy handles lifecycle Real class untouched Β· add auth/lazy-init in proxy only
Tactic 3 β€” Dependency Injection (DI)
❌ Without DI β€” OrderService creates its own dependencies
Problem: OrderService uses new to create concrete implementations β€” tightly coupled, untestable, and impossible to swap without changing the class.
OrderService new PaymentServiceImpl() new ShippingServiceImpl() new NotificationServiceImpl() PaymentServiceImpl ShippingServiceImpl NotificationServiceImpl ⚠ Hardcoded concrete types β€” can't swap or test in isolation Swap PaymentServiceImpl β†’ must edit OrderService constructor
✓ With DI β€” dependencies injected from outside
Solution: Dependencies are injected via constructor. A DI container wires the concrete implementations at startup β€” OrderService depends only on interfaces.
DI Container resolves at runtime OrderService constructor(PaymentService, ShippingService, NotifService) PaymentService ShippingService NotificationSvc βœ“ Swap PayPalService at startup β€” zero changes in OrderService
📋 Chapter 2 — Summary
  • 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
03
Chapter Three Β· Reduce Coupling

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
Five Tactics for Call Dependencies
πŸ”Œ
1. Abstractions

Depend on interfaces, not concrete implementations. Callers are decoupled from who implements the contract.

πŸ”„
2. Adapter Pattern

Bridge incompatible interfaces. Allows a component to call another without direct structural coupling.

πŸ”
3. Avoid Cyclical Relationships

Circular call dependencies (A→B→A) create unresolvable coupling. Use architecture layers to prevent them.

πŸ’¬
4. Asynchronous Messaging

Replace direct calls with messages via a broker. Sender decoupled from receiver β€” doesn't even need to know the receiver exists.

🚧
5. Restrict Communication

Enforce communication rules (layers, APIs, gateways). Only necessary connections allowed β€” prevents spaghetti coupling.

Tactic 1 β€” Using Abstractions (Interfaces)
❌ Without Abstraction β€” direct concrete dependency
Challenge: OrderService depends directly on CreditCardProcessor. Swapping payment providers requires changing the caller β€” tight call coupling.
OrderService new PaymentServiceImpl() new ShippingServiceImpl() new NotificationServiceImpl() PaymentServiceImpl ShippingServiceImpl NotificationServiceImpl ⚠ Hardcoded concrete types β€” can't swap or test in isolation Swap PaymentServiceImpl β†’ must edit OrderService constructor
✓ With Abstraction β€” depend on interface
Principle: Define a common PaymentProcessor interface. OrderService depends only on the interface β€” any implementation can be swapped without touching the caller.
PaymentProcessor Β«interfaceΒ» + processPayment(amount) OrderService depends on interface CreditCardProcessor PayPalProcessor BankPaymentAdapter βœ“ Swap any implementation β€” OrderService never changes
Tactic 2 β€” Adapter Pattern
❌ Without Adapter β€” incompatible interfaces
Problem: OrderService calls processPayment(amount) but the bank provides BankAccountProcessor with debitAccount(amount, accountId) β€” incompatible signatures, can't plug in directly.
OrderService calls: processPayment(amt) PaymentProcessor Β«interfaceΒ» + processPayment(amt) BankAccountProcessor + debitAccount(amt, accountId) Β«third-party β€” incompatibleΒ» βœ— incompatible ⚠ Need Adapter to convert interface
✓ With Adapter β€” bridged to the expected interface
Solution: BankPaymentAdapter implements PaymentProcessor and wraps BankAccountProcessor, translating processPayment(amt) β†’ debitAccount(amt, id). Neither side changes.
PaymentProcessor Β«interfaceΒ» + processPayment(amt) OrderService calls interface only BankPaymentAdapter + processPayment(amt) β†’ calls debitAccount() BankAccount Processor (third-party) processPayment(amt) β†’ debitAccount(amt, "default") βœ“ No changes
Tactic 3 β€” Avoid Cyclical (Circular) Relationships
❌ Cyclical Dependencies β€” Problematic
  • 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
Component A Component B Component C calls calls calls ⚠ Circular loop β€” can't deploy or test independently
✓ Layered Architecture β€” No Cycles
  • 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
Presentation Layer Business Logic Layer Data Access Layer Database one-way ↓ only
Tactic 4 β€” Asynchronous Messaging
Pattern: Microservice A sends a message to a Message Broker / Queue. Microservice B subscribes and processes the message asynchronously. A and B never directly call each other.
  • 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
Microservice A (Sender) Message Broker / Queue Microservice B (Subscriber) publish consume
Tactic 5 β€” Restrict Communication
❌ Unrestricted β€” Every component calls every other
  • 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
A B C D E ⚠ Spaghetti β€” every node coupled
✓ Restricted β€” Only necessary connections allowed
  • 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
Client API Gateway enforces rules Service A Service B Service C βœ— direct call blocked βœ“ Gateway enforces permitted routes β€” no ad-hoc shortcuts
📋 Chapter 3 — Summary
  • 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
04
Chapter Four Β· Coupling Types

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
Definitions
πŸ”—
Static Coupling
  • 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() inside OrderService constructor
⚑
Dynamic Coupling
  • 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
E-Commerce Application β€” Code Examples
Static Coupling β€” concrete class hardcoded at compile time
public class OrderService { private PaymentService paymentService; public OrderService() { // Hardcoded β€” static coupling this.paymentService = new CreditCardService(); } public void placeOrder(double amount) { paymentService.processPayment(amount); } }
Dynamic Coupling β€” resolved at runtime via DI
public class OrderService { private PaymentService paymentService; // Which impl? Only known at runtime public OrderService( PaymentService paymentService) { this.paymentService = paymentService; } public void placeOrder(double amount) { paymentService.processPayment(amount); } }
Data Dependency Examples β€” Static vs Dynamic
Static β€” hardcoded IP address
# App 1 β†’ App 2 via hardcoded URL API_URL = "http://192.168.1.100:8081/api/data" response = requests.get(API_URL) # Change the IP β†’ must change code & redeploy
Dynamic β€” service discovery resolves at runtime
# App 1 β†’ Service Discovery β†’ App 2 API_URL = "http://app2.domain/api/data" response = requests.get(API_URL) # DNS resolves "app2.domain" at runtime # Move App 2 β†’ update discovery only
Advantages & Disadvantages
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
Important Caveat
⚠ Moving from static to dynamic coupling does NOT necessarily reduce coupling.

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.
Dynamic β‰  Decoupled β€” DI still couples to PayPalService
DI Assembler resolves at runtime OrderService PayPalService injects still coupled ⚠ βœ“ Dynamic β€” resolved at runtime ⚠ Coupling still EXISTS β€” just resolved at a different time Swap PayPalService β†’ still need to change the wiring
Monolith β†’ Microservices β‰  automatic decoupling
Monolith High Cohesion Static Analysis βœ“ Compile Errors βœ“ β†’ Poor Split (Low Cohesion) Service A Service B Service C Service D βœ— No static analysis βœ— No compile-time error detection Low Cohesion β†’ Tight Coupling, now harder to detect
Rule of thumb: Use static coupling as the default β€” it's more predictable and maintainable. Use dynamic coupling selectively where runtime flexibility genuinely adds value (config-driven deployments, plugin architectures, service discovery).
📋 Chapter 4 — Summary
  • 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
Summary β€” All Four Coupling Topics at a Glance
01 Β· Structural

Structural Dependencies

  • Inheritance β€” tightest coupling
  • Type usage β€” concrete type dependency
  • Polymorphism β€” program to interfaces
  • FaΓ§ade β€” hide third-party complexity
  • Decorator β€” compose over inherit
02 Β· Instantiation

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
03 Β· Call

Call Dependencies

  • Method calls & network calls
  • Abstractions (interfaces)
  • Adapter Pattern
  • Avoid cyclical relationships
  • Async Messaging via broker
  • Restrict Communication
04 Β· Static vs Dynamic

Static vs Dynamic Coupling

  • Static = compile-time wiring
  • Dynamic = runtime resolution
  • Static: tooling, performance, clarity
  • Dynamic: flexibility, extensibility
  • Dynamic ≠ less coupling
iSAQB Β· CPSA-F Study Reference Β· Design Tactics to Reduce Coupling Β· 2026