SOLID & OOP Design business case

Published: (January 20, 2026 at 07:54 AM EST)
4 min read
Source: Dev.to

Source: Dev.to

OOP Design Principles

Encapsulation

Encapsulation means bundling data with the methods that operate on that data and restricting direct access to some of an object’s components.

Examples:

  • Can an employee have a negative salary?
  • Can a person’s age be negative?

These validations are part of business logic and business rules.

Inheritance

Inheritance groups related objects and allows reuse of shared logic.

Example: CreditCardPayment inherits common payment behavior.

Red flag: If you find yourself overriding most of the parent class methods, the inheritance hierarchy may be wrong.

Polymorphism

Polymorphism lets the same method behave differently depending on the object that implements it.

Example: processPayment(amount) – Stripe processes it one way, PayPal another, but the method name stays the same.

Abstraction

Abstraction works hand‑in‑hand with polymorphism by exposing only the necessary features of an object.

Final thought on OOP: When used correctly, these principles reduce bugs, improve maintainability, make systems easier to extend, and keep business rules safe.


SOLID Principles

Single Responsibility Principle (SRP)

A unit should have only one reason to change.

Bad design:

class UserService {
  validateUserData() { /* ... */ }
  saveUserToDatabase() { /* ... */ }
  sendWelcomeEmail() { /* ... */ }
}

Good design:

class UserValidator { /* validates data */ }
class UserRepository { /* handles database */ }
class EmailService { /* sends emails */ }

Open/Closed Principle (OCP)

Software entities should be open for extension but closed for modification.

Instead of hard‑coded conditionals:

if (paymentType == "stripe") { /* ... */ }

Use an abstraction:

interface PaymentGateway { pay(amount: number): void; }

class StripeGateway implements PaymentGateway { /* ... */ }
class PaypalGateway implements PaymentGateway { /* ... */ }

Adding a new provider now only requires a new class, not changes to existing code.

Liskov Substitution Principle (LSP)

Objects of a superclass should be replaceable with objects of a subclass without altering the correctness of the program.

Problematic design:

class Bird { fly() { /* ... */ } }
class Penguin extends Bird { /* cannot fly */ }

Better design:

class Bird { /* common behavior */ }
class FlyingBird extends Bird { fly() { /* ... */ } }
class Penguin extends Bird { /* no fly method */ }

Interface Segregation Principle (ISP)

Clients should not be forced to depend on interfaces they do not use.

Bad:

interface Worker {
  work(): void;
  eat(): void;
}
class RobotWorker implements Worker {
  work() { /* ... */ }
  eat() { /* empty */ }
}

Good:

interface Workable { work(): void; }
interface Eatable { eat(): void; }

class RobotWorker implements Workable { /* ... */ }
class HumanWorker implements Workable, Eatable { /* ... */ }

Dependency Inversion Principle (DIP)

High‑level modules should depend on abstractions, not on concrete implementations.

Bad:

class OrderService {
  private gateway = new StripePaymentGateway();
}

Good:

class OrderService {
  private gateway: PaymentGateway; // injected
}

Final thought: OOP provides the language; SOLID supplies the grammar.


Business Use Case: Static Factory Method with SOLID

We need a flexible payment orchestration that can add new providers without if/else clutter.

1. Define the contract

interface PaymentGateway {
  pay(amount: number): void;
}

2. Implement concrete gateways

class StripeGateway implements PaymentGateway {
  pay(amount: number): void {
    console.log(`💳 Paid ${amount}$ using Stripe`);
  }
}

class PaypalGateway implements PaymentGateway {
  pay(amount: number): void {
    console.log(`💰 Paid ${amount}$ using PayPal`);
  }
}

class CashOnDeliveryGateway implements PaymentGateway {
  pay(amount: number): void {
    console.log(`📦 Cash on Delivery: ${amount}$ will be collected`);
  }
}

3. Create the static factory

class PaymentFactory {
  static create(provider: string): PaymentGateway {
    switch (provider) {
      case "stripe":
        return new StripeGateway();
      case "paypal":
        return new PaypalGateway();
      case "cod":
        return new CashOnDeliveryGateway();
      default:
        throw new Error("Payment provider not supported");
    }
  }
}

4. Use the factory

const gateway = PaymentFactory.create("stripe");
gateway.pay(100);

How the design satisfies SOLID & OOP

  • Encapsulation: Each gateway hides its internal implementation.
  • Polymorphism: All gateways share the pay() method but behave differently.
  • Abstraction: Client code depends only on the PaymentGateway interface.
  • SRP: Each class has a single responsibility (validation, data access, emailing, payment processing, etc.).

Extending the system

If a new provider (e.g., Apple Pay) is required:

class ApplePayGateway implements PaymentGateway {
  pay(amount: number): void {
    console.log(`🍏 Paid ${amount}$ using Apple Pay`);
  }
}

Add it to the factory:

case "applepay":
  return new ApplePayGateway();

No existing classes need modification.

Final thought: This example shows how a static factory method, combined with SOLID principles and OOP, creates a clean, extensible architecture ideal for real‑world payment orchestration platforms.

Back to Blog

Related posts

Read more »

Day-3 details of Class and Object

Class - Class is a Java keyword - Class is a template - Class is a logical entity - First letter of the class name should be capital CamelCase, e.g., SalaryAcc...