Design PatternsBackend ArchitectureProduction Ready13 lessons

SOLID Principles

Learn the object-oriented design principles that keep Java backend systems modular, testable, extensible, and safe to evolve.

Design Patterns

Overview

SOLID is a set of five object-oriented design principles used to control change in software systems. These principles are especially valuable in enterprise backend applications where business rules, integrations, data models, and deployment constraints evolve continuously.

SOLID does not mean adding interfaces everywhere. It means designing code so each part has a clear reason to exist, dependencies point in stable directions, and new behavior can be added without breaking existing behavior.

SOLID principles

  • Single Responsibility Principle
  • Open Closed Principle
  • Liskov Substitution Principle
  • Interface Segregation Principle
  • Dependency Inversion Principle
SOLID Design Goal
Architecture
Changing Requirement | v Small focused change | v Minimal regression risk | v Testable and deployable code

SOLID is about reducing the blast radius of change in growing codebases.

Design Patterns

Why SOLID Exists

SOLID exists because software changes. Backend systems add features, replace dependencies, integrate with new vendors, support new markets, and handle new failure modes. Without design discipline, each change touches too many files and creates accidental coupling.

SOLID reduces

  • Fragile classes that break for unrelated changes
  • Large services with too many responsibilities
  • Hard-coded dependencies on databases, queues, and clients
  • Interfaces that force unused methods on implementers
  • Subclasses that violate parent expectations
Design Pressure in Backend Systems
Architecture
New API field New database rule New payment provider New Kafka event New compliance rule | v Poor design -> many risky edits Good design -> localized extension

Design Patterns

Single Responsibility Principle

The Single Responsibility Principle says a class should have one reason to change. In backend systems, this usually means separating validation, business rules, persistence, mapping, and external communication.

Problem

A single service parses requests, validates business rules, writes to the database, calls external systems, formats responses, and sends emails. Any change in one concern risks breaking another.

javapublic final class OrderService { private final OrderValidator validator; private final OrderRepository repository; private final PaymentGateway paymentGateway; private final OrderEventPublisher eventPublisher; public OrderReceipt placeOrder(PlaceOrderCommand command) { validator.validate(command); Payment payment = paymentGateway.authorize(command.payment()); Order order = Order.create(command, payment); repository.save(order); eventPublisher.publishOrderPlaced(order); return OrderReceipt.from(order); } }

The service coordinates the use case. Validation, persistence, payment, and publishing are separate responsibilities.

SRP Split
Architecture
OrderService |-- coordinates use case OrderValidator |-- validates command OrderRepository |-- persists order PaymentGateway |-- payment integration OrderEventPublisher `-- publishes event

Design Patterns

Open Closed Principle

The Open Closed Principle says software entities should be open for extension but closed for modification. You should be able to add new behavior without repeatedly editing stable, tested code.

In backend systems, this often appears when supporting new payment methods, notification channels, pricing rules, fraud checks, export formats, or tenant policies.

javapublic interface DiscountPolicy { Money apply(Customer customer, Money subtotal); } public final class LoyaltyDiscountPolicy implements DiscountPolicy { public Money apply(Customer customer, Money subtotal) { return customer.isLoyal() ? subtotal.multiply("0.90") : subtotal; } } public final class CheckoutService { private final List<DiscountPolicy> discountPolicies; public Money calculateTotal(Customer customer, Money subtotal) { Money total = subtotal; for (DiscountPolicy policy : discountPolicies) { total = policy.apply(customer, total); } return total; } }
OCP Extension
Architecture
CheckoutService | v DiscountPolicy interface | +-- LoyaltyDiscountPolicy +-- CouponDiscountPolicy `-- SeasonalDiscountPolicy

Design Patterns

Liskov Substitution Principle

The Liskov Substitution Principle says objects of a subtype should be usable wherever the parent type is expected without changing correctness. Subtypes must honor the parent contract.

Violations happen when a subclass weakens guarantees, throws unexpected exceptions, ignores required behavior, or changes meaning.

javapublic interface MessagePublisher { void publish(DomainEvent event); } public final class KafkaMessagePublisher implements MessagePublisher { public void publish(DomainEvent event) { // publish to Kafka with configured topic and key } } public final class InMemoryMessagePublisher implements MessagePublisher { public void publish(DomainEvent event) { // store event for tests } }

Both implementations honor the same expectation: accepting a DomainEvent and attempting to publish it according to the environment.

Substitution Contract
Architecture
Client code depends on MessagePublisher | v any implementation must satisfy | +-------+-------+ | | Kafka publisher Test publisher

Design Patterns

Interface Segregation Principle

The Interface Segregation Principle says clients should not be forced to depend on methods they do not use. Large interfaces create accidental coupling and weak implementations.

Backend systems often violate ISP with one giant client interface for read, write, admin, reporting, export, and maintenance operations.

javapublic interface CustomerReader { Optional<Customer> findById(CustomerId customerId); } public interface CustomerWriter { void save(Customer customer); } public interface CustomerExporter { InputStream exportCustomers(CustomerExportRequest request); }

Consumers depend only on the capability they need.

ISP Split
Architecture
Large CustomerRepository |-- read |-- write |-- export |-- admin Segregated contracts: CustomerReader CustomerWriter CustomerExporter CustomerAdmin

Design Patterns

Dependency Inversion Principle

The Dependency Inversion Principle says high-level policy should not depend directly on low-level details. Both should depend on abstractions.

In backend applications, domain and application services should not be tightly coupled to specific databases, HTTP clients, message brokers, or cloud SDKs.

javapublic interface PaymentGateway { PaymentAuthorization authorize(PaymentRequest request); } public final class CheckoutService { private final PaymentGateway paymentGateway; public CheckoutService(PaymentGateway paymentGateway) { this.paymentGateway = paymentGateway; } public OrderReceipt checkout(CheckoutCommand command) { PaymentAuthorization authorization = paymentGateway.authorize(command.paymentRequest()); return OrderReceipt.authorized(authorization); } }
DIP Dependency Direction
Architecture
CheckoutService | v PaymentGateway interface ^ | StripePaymentGateway implementation

The high-level service depends on a stable contract, not the vendor SDK.

Design Patterns

Common Violations

Violation Symptoms
Architecture
Change request arrives | v Many unrelated files change | v Tests require excessive mocking | v Regression risk increases

Design Patterns

Spring Boot Examples

Spring Boot naturally supports SOLID when dependencies are injected through constructors and behavior is separated by responsibility.

java@Service public class InvoiceService { private final InvoiceRepository invoiceRepository; private final TaxPolicy taxPolicy; private final InvoiceEventPublisher eventPublisher; public InvoiceService( InvoiceRepository invoiceRepository, TaxPolicy taxPolicy, InvoiceEventPublisher eventPublisher) { this.invoiceRepository = invoiceRepository; this.taxPolicy = taxPolicy; this.eventPublisher = eventPublisher; } }
Spring Bean Composition
Architecture
Controller | v Service | +-- Repository +-- Policy `-- Publisher

Design Patterns

Microservice Examples

SOLID applies inside microservices and across service boundaries. A microservice should have a focused business capability, but internal code still needs clear responsibilities.

Examples

  • PaymentService depends on PaymentGateway, not a specific provider SDK
  • OrderService publishes OrderPlacedEvent through an event publisher abstraction
  • PricingService composes pricing rules through policy interfaces
  • CustomerService separates read models from write commands
Microservice Port Adapter Design
Architecture
Application Core |-- business rules |-- use cases `-- ports |-- PaymentGateway |-- OrderRepository `-- EventPublisher Adapters |-- StripePaymentGateway |-- PostgresOrderRepository `-- KafkaEventPublisher

Design Patterns

Production Notes

From Design to Operations
Architecture
Focused components | v Clear tests | v Small deployments | v Fast diagnosis | v Safer production changes

Design Patterns

Interview Questions

  1. What problem does SOLID solve?
  2. What is the Single Responsibility Principle?
  3. How do you identify multiple reasons to change?
  4. What does open for extension and closed for modification mean?
  5. How can Strategy support the Open Closed Principle?
  6. What is the Liskov Substitution Principle?
  7. Give an example of an LSP violation.
  8. What is the Interface Segregation Principle?
  9. How can large repository interfaces violate ISP?
  10. What is the Dependency Inversion Principle?
  11. How does constructor injection support DIP?
  12. How does Spring Boot help with SOLID?
  13. Can SOLID be over-engineered?
  14. How does SOLID help testing?
  15. How does SOLID reduce production risk?

Design Patterns

Best Practices

Practical SOLID checklist

  • Keep use cases focused and readable
  • Extract policies when business rules vary
  • Hide infrastructure behind interfaces owned by the application
  • Use constructor injection for required dependencies
  • Keep interfaces aligned to client needs
  • Prefer composition over inheritance
  • Write tests against behavior, not implementation detail
  • Introduce abstractions when change patterns justify them
Healthy Backend Design
Architecture
Controller -> Use Case Service -> Domain Policy | +-> Repository Port -> Database Adapter | `-> Event Port -> Kafka Adapter