Java Fundamentals Tutorial 0/33 lessons ~6 min read Lesson 8

    SOLID Principles

    SOLID is not a checklist for code reviews — it is the engineering vocabulary staff architects use to explain why one design scales to 200 microservices and another collapses at 20.

    Course progress0%
    Focus
    21 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    Introduction

    SOLID is not a checklist for code reviews — it is the engineering vocabulary staff architects use to explain why one design scales to 200 microservices and another collapses at 20. Coined by Robert C. Martin ("Uncle Bob"), SOLID transforms five OOP principles into actionable rules for enterprise Java:

    S — Single Responsibility: one class, one reason to change. O — Open/Closed: open for extension, closed for modification. L — Liskov Substitution: subtypes must be substitutable without surprises. I — Interface Segregation: clients depend only on methods they use. D — Dependency Inversion: depend on abstractions, not concretions.

    Spring Boot is built on SOLID — constructor injection (D), interface-based repositories (I), @Profile strategy beans (O), and focused @Service classes (S). Violating SOLID in a payment platform means every new payment rail requires editing core transfer logic, every gateway swap breaks half the codebase, and every PR touches the same 2,000-line god class. This lesson teaches SOLID through banking refactorings you can apply immediately in Spring Boot services.

    Business problem

    Teams that ignore SOLID pay in velocity, incidents, and hiring:

    • God classes: PaymentService validates, processes, sends email, generates PDF, logs audit — one change to email template risks breaking payment processing.
    • Modification cascades: Adding cryptocurrency payments requires editing TransferService, NotificationService, and ReportService — 3 teams, 3 PRs, 3 merge conflicts.
    • Fragile tests: Unit test for overdraft logic requires mocking database, SMTP server, and PDF library — because one class does everything (SRP violation).
    • Concrete coupling: new StripeGateway() hardcoded in service — switching to Adyen requires rewriting business logic, not swapping a bean.

    Why this topic exists

    SOLID exists because unstructured OOP degrades into worse spaghetti than procedural code:

    • Change isolation: SRP ensures a bug in email notification cannot corrupt payment calculation — different classes, different deploy risk.
    • Safe extension: OCP lets you add FedNow rail without touching existing ACH code — new class, zero modification of tested paths.
    • Correct polymorphism: LSP guarantees SavingsAccount can replace Account anywhere without breaking transfer invariants.
    • Minimal coupling: ISP prevents FraudService from depending on 15 unused AccountRepository methods.
    • Testability and swap: DIP enables Spring to inject mock PaymentProcessor in tests and real SWIFT client in production.

    Core concepts

    SOLID — definitions with banking context:

    • S — Single Responsibility Principle (SRP): A class should have only one reason to change. PaymentValidator validates; PaymentProcessor processes; PaymentNotifier sends alerts. Not one class doing all three.
    • O — Open/Closed Principle (OCP): Open for extension (new payment types), closed for modification (existing transfer flow unchanged). Add CryptoPaymentProcessor implements PaymentProcessor — no edits to TransferService.
    • L — Liskov Substitution Principle (LSP): Subtypes must honor the parent contract. Account acc = new SavingsAccount(); acc.withdraw(money) must not throw unexpected exceptions or return wrong types.
    • I — Interface Segregation Principle (ISP): No client should depend on methods it doesn't use. Split AccountOperations into ReadableAccount + WritableAccount — read-only reporting service depends only on read interface.
    • D — Dependency Inversion Principle (DIP): High-level modules (TransferService) depend on abstractions (PaymentProcessor interface), not low-level modules (StripeGateway concrete class). Spring @Autowired/@Inject enforces this.

    Internal architecture

    SOLID-compliant payment microservice architecture:

    text
    ┌──────────────────── High Level (depends on abstractions) ────────────────────┐
    │ TransferService ──depends on──▶ PaymentProcessor (interface) [D - DIP] │
    │ ──depends on──▶ AccountRepository (interface) │
    │ ──depends on──▶ PaymentNotifier (interface) │
    └───────────────────────────────┬──────────────────────────────────────────────┘
    │ implements / injected by Spring
    ┌───────────────────────┼───────────────────────┐
    ▼ ▼ ▼
    ACHProcessor WireTransferProcessor CryptoProcessor [O - OCP]
    (new rail = new class) (existing unchanged) (extension only)
    │ │ │
    └───────────────────────┼───────────────────────┘
    ┌───────────────────────────────▼──────────────────────────────────────────────┐
    │ PaymentValidator [S] │ AuditLogger [S] │ EmailNotifier [S] │
    │ (one reason to change each) │
    └──────────────────────────────────────────────────────────────────────────────┘
    ReadableAccount (interface) [I] WritableAccount (interface) [I]
    │ │
    └──────── AccountRepository ─────────┘
    CheckingAccount / SavingsAccount [L - substitutable subtypes]

    SOLID principle map — five diagrams showing each rule in a payment system:

    S — Single Responsibility
    PaymentValidator
    Validates only
    PaymentProcessor
    Processes only
    PaymentNotifier
    Notifies only
    One class, one reason to change — not one god service.
    O — Open/Closed
    TransferService
    Closed for edit
    PaymentProcessor
    Interface
    CryptoProcessor
    New extension
    Add payment rails by new classes — never modify TransferService.
    D — Dependency Inversion
    TransferService
    High-level module
    PaymentProcessor
    Abstraction
    ACHProcessor
    Low-level impl
    Spring injects concrete bean — service depends on interface.
    I — Interface Segregation
    ReadableAccount
    getBalance()
    WritableAccount
    withdraw()
    ReportService
    Uses read only
    Reporting service never sees write methods it must not call.

    Code walkthrough

    Before/after refactoring — all five SOLID principles applied to a payment service:

    • SRP refactor: Extracted PaymentValidator, EmailNotifier, ConsoleAuditWriter — each changes for one reason only.
    • OCP refactor: New CryptoPaymentProcessor added — BadPaymentService would need if/else edit; TransferService untouched.
    • DIP refactor: TransferService depends on PaymentProcessor interface — Spring injects ACH or Crypto bean at runtime.
    • ISP refactor: Separate PaymentNotifier and AuditWriter — not one fat Operations interface with 20 methods.
    java
    // ═══════════════════════════════════════════════════════════════
    // BEFORE — violates SRP, OCP, DIP (common enterprise anti-pattern)
    // ═══════════════════════════════════════════════════════════════
    class BadPaymentService {
    public void processPayment(String from, String to, BigDecimal amount) {
    // SRP violation: validation + processing + notification + audit in one method
    if (amount.compareTo(BigDecimal.ZERO) <= 0) throw new IllegalArgumentException("bad amount");
    StripeGateway stripe = new StripeGateway(); // DIP violation: concrete dependency
    stripe.charge(from, amount);
    sendEmail(from, "Payment sent"); // SRP: notification mixed in
    writeAuditLog(from, to, amount); // SRP: audit mixed in
    // OCP violation: adding crypto means editing this method with if/else
    if ("CRYPTO".equals(getPaymentType())) { /* new branch */ }
    }
    }
    // ═══════════════════════════════════════════════════════════════
    // AFTER — SOLID-compliant Spring Boot design
    // ═══════════════════════════════════════════════════════════════
    // [I] Interface Segregation — focused contracts
    interface PaymentProcessor { PaymentResult process(Payment payment); }
    interface PaymentNotifier { void notify(PaymentEvent event); }
    interface AuditWriter { void write(AuditEntry entry); }
    // [S] Single Responsibility — one class per concern
    class PaymentValidator {
    public void validate(Payment payment) {
    if (payment.amount().compareTo(BigDecimal.ZERO) <= 0)
    throw new IllegalArgumentException("Amount must be positive");
    }
    }
    // [O + D] Open for extension via new implementations; depend on interface
    class ACHPaymentProcessor implements PaymentProcessor {
    public PaymentResult process(Payment payment) {
    System.out.println("ACH: processing " + payment.amount());
    return PaymentResult.success();
    }
    }
    class CryptoPaymentProcessor implements PaymentProcessor {
    public PaymentResult process(Payment payment) {
    System.out.println("Crypto: processing " + payment.amount());
    return PaymentResult.success();
    }
    }
    // [D] High-level service depends on abstractions — Spring constructor injection
    class TransferService {
    private final PaymentValidator validator;
    private final PaymentProcessor processor; // interface — not StripeGateway
    private final PaymentNotifier notifier;
    private final AuditWriter auditWriter;
    TransferService(PaymentValidator v, PaymentProcessor p,
    PaymentNotifier n, AuditWriter a) {
    this.validator = v; this.processor = p;
    this.notifier = n; this.auditWriter = a;
    }
    public PaymentResult transfer(Payment payment) {
    validator.validate(payment); // S: delegated
    PaymentResult result = processor.process(payment); // D + O: polymorphic
    auditWriter.write(new AuditEntry(payment)); // S: delegated
    notifier.notify(new PaymentEvent(payment)); // S: delegated
    return result;
    }
    }
    // Demo — swap processor without changing TransferService [OCP + DIP]
    record Payment(String from, String to, BigDecimal amount) {}
    record PaymentResult(boolean ok) { static PaymentResult success() { return new PaymentResult(true); } }
    record PaymentEvent(Payment p) {}
    record AuditEntry(Payment p) {}
    class EmailNotifier implements PaymentNotifier {
    public void notify(PaymentEvent e) { System.out.println("Email sent to " + e.p().from()); }
    }
    class ConsoleAuditWriter implements AuditWriter {
    public void write(AuditEntry e) { System.out.println("Audit: " + e.p().from() + " → " + e.p().to()); }
    }
    class SOLIDDemo {
    public static void main(String[] args) {
    Payment p = new Payment("ACC-1", "ACC-2", new BigDecimal("250.00"));
    // Wire with ACH processor
    TransferService ach = new TransferService(
    new PaymentValidator(), new ACHPaymentProcessor(),
    new EmailNotifier(), new ConsoleAuditWriter());
    ach.transfer(p);
    // Swap to Crypto — TransferService unchanged [OCP]
    TransferService crypto = new TransferService(
    new PaymentValidator(), new CryptoPaymentProcessor(),
    new EmailNotifier(), new ConsoleAuditWriter());
    crypto.transfer(p);
    }
    }
    /*
    * Expected output:
    * ACH: processing 250.00
    * Audit: ACC-1 → ACC-2
    * Email sent to ACC-1
    * Crypto: processing 250.00
    * Audit: ACC-1 → ACC-2
    * Email sent to ACC-1
    */

    Production example

    Production Spring Boot — SOLID in action:

    • Constructor injection (DIP): TransferService never imports Stripe or ACH classes — only PaymentProcessor interface.
    • @Profile (OCP): Enable wire or ACH via application.yml — new processor = new @Service class.
    • @EventListener (SRP): Audit and email are separate listeners — adding SMS notification = new listener, zero TransferService changes.
    • @Transactional boundary: TransferService owns transaction; side effects async via events — clear responsibility.
    java
    // [D + I] Depend on focused interfaces
    @Service
    public class TransferService {
    private final PaymentValidator validator;
    private final PaymentProcessor processor;
    private final ApplicationEventPublisher events;
    public TransferService(PaymentValidator validator,
    PaymentProcessor processor,
    ApplicationEventPublisher events) {
    this.validator = validator;
    this.processor = processor;
    this.events = events;
    }
    @Transactional
    public PaymentResult transfer(PaymentCommand cmd) {
    validator.validate(cmd);
    PaymentResult result = processor.process(cmd.toPayment());
    events.publishEvent(new PaymentCompletedEvent(cmd, result)); // S: async side effects
    return result;
    }
    }
    // [O + D] Extension via @Profile — add rail without editing TransferService
    @Service
    @Profile("ach")
    public class ACHPaymentProcessor implements PaymentProcessor { ... }
    @Service
    @Profile("wire")
    public class WirePaymentProcessor implements PaymentProcessor { ... }
    // [S] Single responsibility listeners
    @Component
    public class PaymentAuditListener {
    @EventListener
    public void onPaymentCompleted(PaymentCompletedEvent event) {
    auditRepository.save(event.toAuditEntry());
    }
    }
    @Component
    public class PaymentEmailListener {
    @EventListener
    public void onPaymentCompleted(PaymentCompletedEvent event) {
    emailService.sendReceipt(event.customerEmail());
    }
    }

    Enterprise case study

    Amazon — SOLID at service scale: Amazon's internal Java service guidelines enforce DIP via constructor injection and ban field @Autowired on concrete classes. When Amazon Payments added Buy Now Pay Later (BNPL), the team implemented BNPLPaymentProcessor implements PaymentProcessor — zero modifications to checkout flow that had processed billions of card transactions. Audit, email, and fraud checks ran via existing event listeners (SRP). Contrast: a team that violated OCP with switch-on-payment-type in CheckoutService required 6 weeks of regression testing for the same feature.

    • Violation: CheckoutService with 800 lines and switch(paymentType) — SRP + OCP broken.
    • Refactor: Extract PaymentProcessor strategy; event-driven side effects; interface-based repos.
    • Result: BNPL added in 2 weeks with isolated test suite; checkout regression tests unchanged.
    • Metric: PRs touching CheckoutService dropped from 40/month to 3/month after SOLID refactor.

    Performance considerations

    SOLID and performance — myths vs reality:

    • Interface dispatch cost: Virtual method call ~1-2ns — negligible vs database/API I/O in payment services. JIT devirtualizes monomorphic calls.
    • SRP vs call overhead: More classes = more method calls — nanoseconds vs milliseconds for network; always favor clarity and change isolation.
    • Event-driven SRP: @EventListener async side effects decouple latency — transfer response not blocked by email/PDF generation.
    • Over-SOLIDification: Interface with one impl and no expected variation — premature abstraction adds indirection without benefit.

    Security considerations

    SOLID supports security architecture:

    • SRP for security: Authorization logic in dedicated PaymentAuthorizer — not scattered across controllers and services.
    • ISP limits blast radius: Read-only reporting service implements ReadableAccount only — cannot invoke withdraw even if compromised.
    • DIP enables security testing: Inject mock FraudChecker in tests; swap production impl without changing TransferService.
    • LSP prevents security bypass: RestrictedAccount subtype must not weaken withdraw validation when substituted for Account.

    Scalability considerations

    SOLID enables team and system scale:

    • Parallel team ownership: SRP boundaries map to team boundaries — Payments team owns Processor, Notifications team owns Notifier.
    • Independent deploy: OCP-compliant extensions deploy as new modules — BNPL processor canary without checkout redeploy.
    • Test pyramid: DIP makes unit tests fast — mock interfaces, no Spring context needed for TransferService logic.
    • Feature flags: @ConditionalOnProperty on PaymentProcessor beans — OCP + Spring enables gradual rollout.

    Production challenges

    Common SOLID violations in enterprise Java codebases:

    • God @Service: OrderService with 60 methods — create, update, cancel, refund, email, report, export, sync — classic SRP violation.
    • Switch on type: switch(paymentType) { case ACH: ... case WIRE: ... } — OCP violation; every new type edits core logic.
    • Field injection: @Autowired StripeGateway gateway; — DIP violation; hard to test, hidden dependency.
    • Fat interface: AccountService with 25 methods — ISP violation; reporting client forced to depend on write operations.
    • LSP break: ReadOnlyAccount extends Account but withdraw() throws UnsupportedOperationException — callers expecting Account contract break.

    Common mistakes

    • Applying SRP so aggressively that every line is a new class — balance cohesion with responsibility.
    • Creating interfaces for everything "because SOLID says so" — YAGNI applies; abstract when variation exists.
    • Confusing OCP with "never modify any code" — fix bugs in place; OCP means extend behavior without changing stable abstractions.
    • Assuming inheritance satisfies LSP automatically — overriding with throws or silent behavior change breaks substitutability.
    • Using @Autowired field injection in Spring — constructor injection is DIP-compliant and test-friendly.

    Debugging guide

    Identify SOLID violations during code review or incidents:

    • SRP smell: Class imports from 5+ unrelated packages (email, PDF, SQL, HTTP, crypto) — split by reason to change.
    • OCP smell: Git blame shows same file edited by every feature team — extract strategy/interface.
    • LSP smell: instanceof checks before calling method on subtype — polymorphism broken.
    • ISP smell: Client throws UnsupportedOperationException or no-op methods — interface too fat.
    • DIP smell: new ConcreteClass() inside @Service — inject interface via constructor.
    bash
    # Find DIP violations — concrete instantiation in services
    grep -rn "new.*Gateway\|new.*Repository\|new.*Client" src/ \
    --include="*Service.java"
    # Find OCP violations — switch on type/enums in business logic
    grep -rn "switch.*[Tt]ype\|switch.*PaymentMethod" src/ --include="*.java"
    # ArchUnit test (enterprise standard)
    // noClasses().that().resideInAPackage("..service..")
    // .should().dependOnClassesThat().resideInAPackage("..gateway.stripe..")

    Best practices

    • One @Service class per use case or domain concern — PaymentValidator, TransferService, RefundService (SRP).
    • Program to interfaces — inject PaymentProcessor, not ACHPaymentProcessor (DIP).
    • Extend via new @Service implementations + @Profile/@Conditional — not switch statements (OCP).
    • Split fat interfaces — ReadableAccount vs WritableAccount for read-only clients (ISP).
    • Subtypes must not throw unexpected exceptions or weaken preconditions (LSP).
    • Use constructor injection exclusively in Spring Boot 3.x services (DIP + testability).
    • Enforce with ArchUnit in CI — automated SOLID regression tests on every PR.

    Anti-patterns

    • God Service: 2,000-line @Service — violates SRP; split by bounded context.
    • Switch payment type: if/else or switch on enum in TransferService — violates OCP; use strategy pattern.
    • Field @Autowired: Hidden dependency, cannot make field final — violates DIP.
    • Empty interface impl: ReportService implements AccountService but 20 methods throw — violates ISP.
    • Square extends Rectangle: Classic LSP violation — setWidth breaks square invariant.

    Staff engineer notes

    • Staff reviews ask "which SOLID principle does this violate?" — faster than debating taste.
    • SOLID is not about more classes — it is about change isolation. If two things change for different reasons, split them.
    • Spring Boot's entire design assumes DIP — fighting it with new ConcreteClass() in services fights the framework.
    • ArchUnit tests encode SOLID as law: "services must not depend on gateway implementations" — enforce in CI, not code review memory.
    • OCP is the most misunderstood — it does not forbid modification; it forbids modifying stable, tested core logic when extending behavior.

    Interview questions

    Interview preparation

    15 questions grouped by difficulty — expand any card for a model answer and follow-up probe.

    Beginner

    5
    1. 1What does SOLID stand for?
      Beginner

      Model answer

      • Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion
      • five OOP design principles for maintainable, extensible code. Each addresses a specific coupling or rigidity problem.

      Follow-up probe

      Which is most important?

    2. 2Explain Single Responsibility Principle with an example.
      Beginner

      Model answer

      • A class should have one reason to change. PaymentValidator only validates
      • changes when validation rules change. PaymentProcessor only processes
      • changes when gateway API changes. Bad: one class validates, processes, emails, and audits.

      Follow-up probe

      How small should a class be?

    3. 3Explain Open/Closed Principle.
      Beginner

      Model answer

      • Software entities open for extension, closed for modification. Add CryptoPaymentProcessor implements PaymentProcessor
      • new behavior without editing TransferService. Contrast: adding if/else branch to existing method violates OCP.

      Follow-up probe

      Does OCP mean never modify code?

    4. 4Explain Liskov Substitution Principle.
      Beginner

      Model answer

      • Subtypes must be substitutable for base types without breaking behavior. Account acc = new SavingsAccount(); acc.withdraw(amount) must work as callers expect. Violation: ReadOnlyAccount.withdraw() throws
      • callers of Account break.

      Follow-up probe

      Square extends Rectangle — LSP violation?

    5. 5Explain Dependency Inversion Principle.
      Beginner

      Model answer

      High-level modules depend on abstractions, not low-level concretions.

      TransferService depends on PaymentProcessor interface, not StripeGateway.

      Spring injects concrete bean.

      Enables testing with mocks and swapping implementations.

      Follow-up probe

      Field vs constructor injection?

    Intermediate

    5
    1. 6Explain Interface Segregation Principle.
      Intermediate

      Model answer

      • Clients should not depend on interfaces they don't use. Split fat AccountService (25 methods) into ReadableAccount + WritableAccount. ReportService depends only on ReadableAccount
      • cannot accidentally call withdraw.

      Follow-up probe

      When is a fat interface OK?

    2. 7How does Spring Boot enforce SOLID?
      Intermediate

      Model answer

      DIP: constructor injection of interfaces.

      OCP: @Profile/@Conditional beans for strategy variants.

      SRP: separate @Service, @Component listeners.

      ISP: Spring Data repository interfaces expose only needed CRUD.

      LSP: your domain subtypes must honor contracts.

      Follow-up probe

      What breaks DIP in Spring?

    3. 8Refactor switch(paymentType) to satisfy OCP.
      Intermediate

      Model answer

      • Extract PaymentProcessor interface with process() method. Each type becomes a class: ACHProcessor, WireProcessor, CryptoProcessor. TransferService depends on PaymentProcessor
      • Spring injects correct bean via @Profile or factory. New type = new class, zero TransferService edits.

      Follow-up probe

      Strategy vs factory pattern?

    4. 9How do you test DIP-compliant services?
      Intermediate

      Model answer

      Constructor-inject interfaces.

      In unit test: new TransferService(mockValidator, mockProcessor, mockNotifier, mockAudit).

      No Spring context.

      thenReturn(success).

      Fast, isolated, deterministic.

      Follow-up probe

      @SpringBootTest vs pure unit test?

    5. 10Give an LSP violation in banking code.
      Intermediate

      Model answer

      • PremiumAccount extends Account. Account.withdraw allows any amount. PremiumAccount.withdraw throws if balance would drop below $10,000 minimum
      • callers expecting normal withdraw behavior break. Fix: separate PremiumWithdrawPolicy or compose instead of inherit.

      Follow-up probe

      How to detect LSP violations?

    Advanced

    5
    1. 11Design SOLID-compliant payment system for 5 payment rails.
      Advanced

      Model answer

      PaymentProcessor interface (D). One @Service per rail: ACH, Wire, Card, Crypto, BNPL (OCP). TransferService injects List or factory (D). PaymentValidator, AuditListener, EmailListener separate (SRP). ReadableAccount for reporting (ISP). Account subtypes substitutable (LSP). ArchUnit enforces package rules.

      Follow-up probe

      How route to correct processor?

    2. 12Write ArchUnit rules for SOLID in a Spring Boot monorepo.
      Advanced

      Model answer

      areInterfaces() or resideInPackage(domain).

      impl).

      haveOnlyPrivateConstructors or beSpringComponents.

      accessClassesWithNameMatching('*Gateway') from service layer.

      Follow-up probe

      False positives?

    3. 13When does SOLID conflict with YAGNI?
      Advanced

      Model answer

      • Creating PaymentProcessor interface with one implementation and no planned variation
      • premature abstraction. Apply SOLID when second implementation appears or change frequency justifies boundary. SRP always applies; OCP/DIP/ISP intensify with scale and team count.

      Follow-up probe

      Rule of three?

    4. 14Refactor 2,000-line OrderService — approach?
      Advanced

      Model answer

      • 1) Identify reasons to change (validation, persistence, notification, pricing, inventory). 2) Extract each as focused class (SRP). 3) Introduce interfaces for swappable deps (DIP). 4) Replace switch on order type with strategy (OCP). 5) Split fat repo interface (ISP). 6) Verify subtypes substitutable (LSP). 7) Strangler fig
      • migrate one use case at a time with parallel tests.

      Follow-up probe

      How avoid big-bang rewrite?

    5. 15SOLID vs microservices — relationship?
      Advanced

      Model answer

      • SOLID inside service; bounded context between services. SRP often maps to microservice boundary
      • but microservice != single class SRP. DIP across services via API contracts (REST/events). OCP: new service for new capability vs modifying existing. Violating SOLID inside a microservice creates distributed monolith pain.

      Follow-up probe

      Shared library vs duplication?

    Hands-on exercise

    Lab: Refactor to SOLID — run the playground, then refactor:

    • Run BadPaymentService demo — observe all concerns in one method.
    • Extract PaymentValidator class (SRP) — move validation out of processPayment.
    • Introduce PaymentProcessor interface and ACHPaymentProcessor (DIP + OCP).
    • Refactor to TransferService with constructor injection — run again, same output.
    • Add CryptoPaymentProcessor without editing TransferService — prove OCP.
    • Bonus: write one ArchUnit-style rule as a comment describing what CI should enforce.

    JavaSOLID Principles

    Starter Templates
    OutputRemote JVM (Piston · Java 15)
    Press Run to execute. Real javac + JVM via remote API. Falls back to simulator on network failure.

    Architecture trade-offs

    • SRP vs cohesion: Split too early → fragmented logic; wait too long → god class. Split when reasons to change diverge.
    • OCP vs YAGNI: OCP wins when extension frequency high; direct modification wins for stable, single-impl code.
    • Interface vs concrete: Interface wins for testability and variation; concrete wins when no variation and team is tiny.
    • Event-driven SRP vs synchronous: Events win for decoupling side effects; synchronous wins when strong consistency required.

    Summary

    SOLID transforms OOP from theory into enterprise engineering discipline. You can now identify violations in god services and switch statements, refactor toward Spring Boot-compliant design with constructor injection and strategy beans, and articulate each principle in architecture review. Next: design patterns that implement SOLID in practice.

    Key takeaways

    • S — one class, one reason to change: Validator, Processor, Notifier separate.
    • O — extend via new PaymentProcessor impls; never edit stable TransferService.
    • L — subtypes (SavingsAccount) must honor Account contract without surprises.
    • I — ReadableAccount vs WritableAccount — clients depend only on what they use.
    • D — depend on PaymentProcessor interface; Spring injects concrete bean.
    Ready to mark this lesson complete?Track your journey across the entire course.