Using bridge pattern to manage multiple dimensions of change
When your Payment module is small, inheritance feels enough for implementing payment in your codebase, you just add a Payment class, then a StripePayment and PayPalPayment and that is it. However, as systems scale, two dimensions of change come in:
- Payment types — one-time, subscription, installments, deferred, crypto.
- Payment gateways — Stripe, PayPal, Adyen, Square, custom enterprise APIs.
If you naively use inheritance, the class hierarchy explodes; you end up having stripeOneTimePayment, StripeSubscriptionPayment, payPalOneTimePayment, PayPalSubscriptionPayment, CryptoDeferredPayment and so on. Now you have a firehose of classes which are hard to maintain. The Bridge Pattern exists to stop the such problems by decoupling abstractions from implementation allowing both to evolve independently.
1. Lets first understand what is the difference between abstraction and implementation
Abstraction layer deals with the high-level concept the client interacts with (e.g., payment types: one-time, subscription, installment) while the implementation layer deals with the low-level detail that makes it real such as gateways like Stripe or PayPal which actually process the money. Abstraction is the what, and the implementation as the how.
By separating the two hierarchies, we build an orthogonal architecture: new payment types don’t affect gateways, and new gateways don’t affect payment type. This gives us a couple of strengths like:
- Predictability – Changes are localized; you know what will break (or not).
- Scalability – New features don’t require rewriting old code.
- Extensibility – Third-party developers can extend one dimension safely.
- Combinatorial Freedom – Independent dimensions can be mixed and matched at runtime.
2. Lets us show bridge pattern using Payment example
In the code below each gateway knows how to process a payment therefore, we can add new gateways like CryptoGateway without touching the payment abstractions.
// Implementation layer
class PaymentGateway {
process(amount) {}
}
class StripeGateway extends PaymentGateway {
process(amount) {
console.log(`Processing $${amount} via Stripe`);
}
}
class PayPalGateway extends PaymentGateway {
process(amount) {
console.log(`Processing $${amount} via PayPal`);
}
}
In the code below each payment type delegates work to a gateway thus we can easily add DeferredPayment, InstallmentPayment, or even EscrowPayment without touching gateway implementations.
Abstration layer
// Abstraction layer
class Payment {
constructor(gateway) {
this.gateway = gateway;
}
}
class SubscriptionPayment extends Payment {
pay(amount) {
this.gateway.process(amount);
}
}
class OneTimePayment extends Payment {
pay(amount) {
this.gateway.process(amount);
}
}
Now we have two axes of change;
How we consume our Payment
// Usage
new SubscriptionPayment(new StripeGateway()).pay(99);
new OneTimePayment(new PayPalGateway()).pay(10);
Our output
Processing $99 via Stripe
Processing $10 via PayPal
With the two axes of change we have two orthogonal hierarchies and infinite combinations with zero class explosion.
3. Managing change at scale.
In production systems, both abstractions and implementations evolve independently. The Bridge gives you the freedom to grow without rewriting existing code.
Adding gateway without touching payment types
class CryptoGateway extends PaymentGateway {
process(amount) {
console.log(`Processing $${amount} in Bitcoin`);
}
}
// Existing abstraction still works:
new OneTimePayment(new CryptoGateway()).pay(200);
adding a new payment type without touching gateways
class InstallmentPayment extends Payment {
pay(amount) {
console.log(`Splitting $${amount} into installments`);
this.gateway.process(amount / 3);
}
}
new InstallmentPayment(new StripeGateway()).pay(300);
Note how each axis evolves independently
4. Bridge pattern aligns with Open-Closed principle from SOLID principles
Both hierarchies are open to extension, closed to modification, if you need new gateways extend PaymentGateway for new payment types extend Payment.
// Add a new gateway
class AdyenGateway extends PaymentGateway {
process(amount) {
console.log(`Processing $${amount} via Adyen`);
}
}
// Add a new payment type
class EscrowPayment extends Payment {
pay(amount) {
console.log(`Holding $${amount} in escrow`);
this.gateway.process(amount);
}
}
new EscrowPayment(new AdyenGateway()).pay(500);
5. Bridge pattern shines when;
- dealing with two (or more) orthogonal axes of change.
- inheritance leads to a Cartesian product of classes.
- you need pluggability (clients can choose their own implementation).
- designing SDKs, APIs, or libraries intended for third-party extension.
javascript book
If this interested you, check out my Javascript Book