Real-World Applied Patterns
Picture this: it’s Friday, 6:30 PM. You’re about to leave the office when your phone rings. It’s the company’s CTO.
"The payment system is down. We’re losing money every minute. Can you take a look?"
That system you hacked together months ago to accept PayPal, then Stripe, then PagSeguro… each with their quirks, all tangled in code only you can understand — and just barely.
That night, while debugging why a PayPal change broke Stripe, you swear: never again.
The Nightmare Every Dev Has Lived
If you’ve ever touched payment systems, you know how it starts:
// "It's just a quick PayPal integration" 🤡
function processPayment(data) {
if (data.amount > 0) {
// Call PayPal API
// Some basic logs
}
}
Then comes Stripe. Then PagSeguro. Then different rules for e-commerce vs subscriptions. Then marketplaces.
In a few weeks, this happens:
// The monster we created 😱
function processPayment(data, gateway, type, options) {
if (gateway === 'paypal') {
if (type === 'ecommerce') {
if (data.amount > 0 && data.customer && options.validateStock) {
try {
// ...
} catch (error) {
// ...
}
}
} else if (type === 'subscription') {
// Almost same, but not really
}
} else if (gateway === 'stripe') {
// You get it...
}
// 500+ lines of pain
}
From Chaos to Clarity with Design Patterns
These situations led me to combine two simple but powerful patterns:
🧱 Template Method: “Define the skeleton of the operation.”
🧠 Strategy: “Plug in specific algorithms where needed.”
Most payments follow the same core steps:
- Validate data
- Prepare for processing (type-specific)
- Process (gateway-specific)
- Finalize (type-specific)
- Log result
Let’s bring some architecture into this.
Template Method: The Unbreakable Backbone
Think of it as a structured routine — predictable, consistent:
abstract class PaymentProcessor {
constructor(protected gateway: PaymentGateway) {}
public async executePayment(data: PaymentData) {
console.log('🚀 Starting processing...');
this.gateway.validateData(data);
await this.preProcess(data);
const result = await this.gateway.process(data);
await this.postProcess(data, result);
this.finalize(result);
return result;
}
protected switchGateway(gateway: PaymentGateway) {
this.gateway = gateway;
}
protected async preProcess(data: PaymentData) {}
protected async postProcess(data: PaymentData, result: PaymentResult) {}
protected async finalize(result: any) {
console.log('Finalizing...');
}
}
✅ Why this is awesome
- Guaranteed validation & logging
- Clear separation of custom logic
- Easy onboarding
- One place to change the core flow
Strategy: Gateway Flexibility
While Template Method gives us structure, Strategy lets us swap out behavior dynamically.
interface PaymentGateway {
getName(): string;
calculateFee(amount: number): number;
process(data: PaymentData): Promise<PaymentResult>;
}
class PayPalGateway implements PaymentGateway {
getName() { return 'PayPal'; }
calculateFee(amount: number) { return amount * 0.034; }
async process(data: PaymentData) {
console.log('💳 Processing with PayPal...');
return {
success: true,
transactionId: `paypal_${Date.now()}`,
fee: this.calculateFee(data.amount)
};
}
}
✅ Strategy wins with:
- Runtime switching
- Easy extension for new gateways
- Fully isolated logic
- Independent testing
Concrete Examples: Different Business Types
🛒 E-commerce
class EcommerceProcessor extends PaymentProcessor {
protected async preProcess(data: PaymentData) {
console.log('🛒 [E-commerce] Checking inventory...');
}
protected async postProcess(data: PaymentData, result: PaymentResult) {
if (result.success) {
console.log('📦 [E-commerce] Preparing shipment...');
} else {
console.log('❌ [E-commerce] Releasing inventory...');
}
}
}
🔄 SaaS/Subscription
class SubscriptionProcessor extends PaymentProcessor {
protected async preProcess(data: PaymentData) {
console.log('🔄 [Subscription] Checking active plan...');
}
protected async postProcess(data: PaymentData, result: PaymentResult) {
if (result.success) {
console.log('📅 [Subscription] Scheduling next charge...');
} else {
console.log('⏸️ [Subscription] Suspending services...');
}
}
}
🏪 Marketplace
class MarketplaceProcessor extends PaymentProcessor {
protected async preProcess(data: PaymentData) {
console.log('🏪 [Marketplace] Validating sellers...');
}
protected async postProcess(data: PaymentData, result: PaymentResult) {
if (result.success) {
console.log('💰 [Marketplace] Distributing payments...');
}
}
}
The Nightmare vs The Dream
🔥 PROBLEMS YOU’VE PROBABLY FACED:
- 500+ line spaghetti function
- One gateway bug breaks all others
- Adding new gateway takes weeks
- No isolation = testing hell
- Fear every deploy
- New devs take forever to onboard
✅ WHAT YOU CAN ACHIEVE:
+ Clean classes, each 30–40 lines
+ Fully isolated gateways
+ Add new gateway in minutes
+ Test everything independently
+ Deploy with confidence
+ Code that explains itself
+ Onboarding in a day
Practical Usage
const store = new EcommerceProcessor(new PayPalGateway());
await store.executePayment({ amount: 150.00, customerId: 'cust_123', paymentMethod: 'card' });
store.switchGateway(new StripeGateway());
await store.executePayment({ amount: 89.90, customerId: 'cust_456', paymentMethod: 'card' });
const saas = new SubscriptionProcessor(new StripeGateway());
await saas.executePayment({ amount: 29.90, customerId: 'sub_789', paymentMethod: 'card' });
Bonus: Smart & A/B Logic
🧪 A/B Testing
class ABTestProcessor extends EcommerceProcessor {
protected async preProcess(data: PaymentData) {
await super.preProcess(data);
const gateway = Math.random() > 0.5 ? new PayPalGateway() : new StripeGateway();
console.log(`🧪 A/B Test: Using ${gateway.getName()}`);
super.switchGateway(gateway);
}
}
🧠 Smart Routing
class SmartProcessor extends PaymentProcessor {
protected async preProcess(data: PaymentData) {
let gateway: PaymentGateway;
if (data.amount < 50) gateway = new StripeGateway();
else if (data.amount < 500) gateway = new PayPalGateway();
else gateway = new PagSeguroGateway();
console.log(`🧠 Smart choice: ${gateway.getName()} for $${data.amount}`);
super.switchGateway(gateway);
}
}
Final Thoughts
This isn’t just a story about design patterns. It’s about how clean architecture transforms your career.
When that dreaded Friday arrives, do you want to be the person putting out fires — or the one who built a system that simply doesn’t break?
Template Method + Strategy won’t solve every problem — but they will solve this one.
Have you lived this payment nightmare? How did you solve it?
Share your story in the comments — others can learn from it.
If this helped you, share it with your team. Good knowledge is shared knowledge.
#designpatterns #cleancode #devlife #typescript #architecture
Top comments (0)