DEV Community

Maicon Jobim
Maicon Jobim

Posted on

How Template Method + Strategy Saved My Payment System (and My Sanity)

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. Validate data
  2. Prepare for processing (type-specific)
  3. Process (gateway-specific)
  4. Finalize (type-specific)
  5. 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...');
  }
}
Enter fullscreen mode Exit fullscreen mode

✅ 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)
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

✅ 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...');
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

🔄 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...');
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

🏪 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...');
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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' });
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

🧠 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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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)