Inheritance Isn't Dead - Your Payment Gateway Proves It

Inheritance Isn't Dead - Your Payment Gateway Proves It

Why composition over inheritance dogma fails in real payment systems, and how to blend both approaches for practical software design.
August 27, 2025

The “composition over inheritance” mantra has been beaten into developers’ heads so thoroughly that we’ve forgotten inheritance actually solves real problems. While everyone’s busy arguing about cats and dogs, payment gateways are quietly proving that inheritance isn’t the villain, dogmatic thinking is.

When Inheritance Actually Makes Sense

Payment gateways don’t care about your ideological purity. They care about consistency, authentication, and not getting sued. When every API call to Opayo requires the same HTTP headers, authentication, and request structure, inheritance isn’t just convenient, it’s responsible design.

Consider the AbstractPaymentRequest pattern: a base class that handles authentication headers, content types, and the actual HTTP request sending. Child classes only need to specify their endpoint and payload. This isn’t academic nonsense, it’s the Template Method Pattern in production, enforcing consistency across dozens of payment operations.

The irony? This inheritance approach actually supports the Open-Closed Principle better than many composition-heavy alternatives. New payment operations become a matter of extending one class rather than configuring multiple components.

1public abstract class AbstractPaymentRequest { 2 protected final String apiKey; 3 4 public final HttpResponse<String> execute() throws Exception { 5 var request = HttpRequest.newBuilder() 6 .uri(URI.create(getEndpoint())) 7 .header("Authorization", "Bearer " + apiKey) 8 .POST(HttpRequest.BodyPublishers.ofString(getPayload())) 9 .build(); 10 return httpClient.send(request, HttpResponse.BodyHandlers.ofString()); 11 } 12 13 protected abstract String getEndpoint(); 14 protected abstract String getPayload(); 15} 16 17public class RefundRequest extends AbstractPaymentRequest { 18 @Override 19 protected String getEndpoint() { return "/refunds"; } 20 21 @Override 22 protected String getPayload() { 23 return "{\"transactionId\":\"" + transactionId + "\"}"; 24 } 25}

Compare this to the composition alternative where you’d need separate classes for authentication, request building, header management, and HTTP execution. When you need to add SSL certificate pinning or request retry logic, inheritance lets you modify one place. Composition forces you to coordinate changes across multiple classes.

Where Composition Saves Your Architecture

Here’s where the purists get it right: payment method handling. When your gateway needs to support cards, PayPal, Apple Pay, and Google Pay, with more coming next quarter, inheritance becomes a nightmare.

1public class PaymentRequest extends AbstractPaymentRequest { 2 private final PaymentMethodBuilder paymentMethodBuilder; 3 4 @Override 5 protected String getPayload() { 6 return "{\"paymentMethod\":" + paymentMethodBuilder.build() + "}"; 7 } 8} 9 10public interface PaymentMethodBuilder { 11 String build(); 12} 13 14public class CardPaymentBuilder implements PaymentMethodBuilder { 15 @Override 16 public String build() { 17 return "{\"type\":\"card\",\"cardNumber\":\"" + cardNumber + "\"}"; 18 } 19}

This composition approach lets you swap payment methods without touching the request logic. New payment method? Implement the PaymentMethodBuilder interface. No regression testing on the entire payment request stack. No fragile inheritance hierarchy that breaks when Visa decides to change their API.

The Blended Reality of Production Systems

The real world doesn’t care about your purity tests. Payment systems use inheritance for consistency where it makes sense (HTTP communication, authentication) and composition for flexibility where it’s needed (payment methods, fraud checks, currency handling).

The most effective architects understand that “prefer composition over inheritance” means exactly that: prefer, not always use. They recognize that:

  • Inheritance excels at enforcing consistency across related operations
  • Composition shines when dealing with variable behaviors that change independently
  • The template method pattern is inheritance’s secret weapon for real-world systems
  • Payment gateways, unlike animals, have concrete business constraints that dictate design choices

The Dogma Costs More Than You Think

Teams that religiously avoid inheritance often end up with:

  • Boilerplate code that manually delegates to components
  • Configuration complexity that rivals their actual business logic
  • Missed opportunities for compile-time safety through type hierarchies
  • Architecture that’s flexible in theory but brittle in practice

Meanwhile, teams that understand both approaches build systems that can handle Stripe’s API changes, PayPal’s quirks, and Apple Pay’s requirements without rewriting their entire payment stack.

The next time someone tells you “inheritance is bad”, ask them how they’d build a payment gateway that needs to maintain consistency across 20+ API endpoints while supporting an ever-growing list of payment methods. The answer probably involves both inheritance and composition, because real engineering isn’t about purity, it’s about solving problems.