📐 Section 18 · Question #20

Can you follow all SOLID principles at once

The SOLID principles are not immutable physical laws or legal statutes. They are engineering heuristics and design guidelines designed to manage and reduce the cost of change in...


🟢 Junior Level

The Short Answer: Theoretically, YES—all five SOLID principles are mutually compatible. Pragmatically in commercial production, NO—applying all five principles dogmatically to every class is not only unnecessary, but actively harmful.

The SOLID principles are not immutable physical laws or legal statutes. They are engineering heuristics and design guidelines designed to manage and reduce the cost of change in complex software systems.

Attempting to enforce all five principles dogmatically across every line of code transforms simple applications into an unreadable labyrinth of hundreds of interfaces, factories, adapters, and indirection layers (The Over-Engineering Trap).

Real-World Analogy: Building a Doghouse vs. a Skyscraper

  • Building a 60-Story Skyscraper: You mandate seismic foundation engineering, redundant elevator shafts, firebreak zoning, and HVAC ducting (Full 5-Pillar SOLID).
  • Building a Backyard Doghouse: Demanding earthquake-proof blueprints, dual electrical backup generators, and industrial elevators for a 3-foot wooden doghouse costs $100,000 and takes six months. A doghouse simply needs to be warm, dry, and sturdy.

Code Comparison: Dogmatic Over-Engineering vs. Pragmatic KISS

Absurd Over-Engineering (Dogmatic SOLID for a 1-Line Calculation)

// 5 files created just to add two integers together:
public interface AdditionOperation {
    int execute(int a, int b);
}

public class IntegerAdditionStrategy implements AdditionOperation {
    @Override
    public int execute(int a, int b) { return a + b; }
}

public class CalculatorService {
    private final AdditionOperation addition;

    public CalculatorService(AdditionOperation addition) { 
        this.addition = addition; 
    }

    public int add(int a, int b) { 
        return addition.execute(a, b); 
    }
}

Pragmatic Senior Java (KISS: Simple, Readable, Maintainable)

public final class Calculator {
    private Calculator() {}

    public static int add(int a, int b) {
        return a + b;
    }
}

🟡 Middle Level

The Fundamental Architectural Trade-offs and Tensions

When you attempt to enforce every SOLID principle simultaneously, you encounter unavoidable architectural trade-offs:

1. SOLID vs. KISS and YAGNI

  • The Conflict: OCP demands creating polymorphic extension points (interfaces, strategy registries, factory abstractions). Meanwhile, YAGNI (You Aren’t Gonna Need It) warns: “Never author abstractions for hypothetical requirements you do not need today.”
  • In $80\%$ of enterprise applications, anticipated “future extensions” never materialize. The codebase remains saddled with dead abstraction layers that increase cognitive overhead.

2. SRP and ISP vs. Coupling (The “Atomic Dust” Anti-Pattern)

  • Dogmatically decomposing classes to the smallest conceivable responsibility (SRP) and slicing interfaces to single-method roles (ISP) creates Atomic Dust (Fowler’s Poltergeists).
  • An engineer must maintain 25 open tabs in their IDE to trace a single user login flow. While intra-class cohesion is high, system-wide Coupling explodes.

3. SOLID vs. Ultra-Low-Latency Performance

  • Every SRP split introduces another heap object allocation, stressing the Garbage Collector.
  • Every DIP/OCP interface invocation is an invokeinterface bytecode operation. If megamorphic, it blocks HotSpot C2 compiler inlining and scalar replacement.

The Gradient Strategy: Applying SOLID by System Maturity

Pragmatic engineering organizations apply SOLID along a spectrum of maturity rather than as a binary mandate:

System Layer / Lifecycle Phase SOLID Strictness Prioritized Principles Architectural Rationale
Prototypes, Spikes & MVPs Minimal KISS, DRY Primary goal is validating product assumptions rapidly in days, not building eternal monuments.
Stateless Utilities Low Static Functions (StringUtils) Pure, stateless algorithms that will never require runtime polymorphic substitution.
High-Throughput Hot-Paths Restricted Procedural Speed Eliminate interface dispatch and heap allocations to achieve microsecond latency SLAs.
Core Domain Business Logic High All 5 SOLID Principles The highest-value, longest-living asset that constantly evolves under business pressure.

🔴 Senior Level

Hardware and JVM Costs of Abstraction Layers

Senior engineers understand the physical hardware costs of clean abstractions under extreme load:

Benchmark: Processing 10,000,000 Transactions (HotSpot C2 64-bit JVM):
┌──────────────────────────────┬──────────────────┬─────────────────┬────────────────┐
│ Architectural Implementation │ Latency (p99)    │ Heap Allocation │ GC Pauses      │
├──────────────────────────────┼──────────────────┼─────────────────┼────────────────┤
│ Direct Procedural Call       │ 1.2 ns           │ 0 bytes         │ 0 ms           │
│ SOLID Strategy + Interfaces  │ 8.5 ns (+600%)   │ 24 bytes / call │ 120 ms (Young) │
└──────────────────────────────┴──────────────────┴─────────────────┴────────────────┘
  1. ITable / VTable Megamorphic Lookup: When an interface has three or more concrete implementations encountering a call site, HotSpot C2 disables Monomorphic Inline Caching. The CPU executes an indirect memory jump via the itable structure, flushing the hardware branch predictor pipeline.
  2. Object Header Overhead and Memory Padding: Decomposing a monolithic entity into five small SRP classes creates five separate object allocations. In a 64-bit JVM with compressed oops (+UseCompressedOops), each object incurs a 12-byte header plus 4-byte padding (16 bytes minimum). At 100,000 operations per second, this generates gigabytes of short-lived heap garbage in the young generation (Eden).

The Expression Problem: OCP Polymorphism vs. Java 21 Sealed Types

The evolution of Java 21 introduces Sealed Types and Pattern Matching for switch, challenging the universal applicability of classic OCP polymorphism:

  • Classic OCP Polymorphism (Open Hierarchies):
    • Easy to add new data types: Create a new class implementing the interface.
    • Extremely difficult to add new operations: Adding a method forces updating 20 existing implementing classes.
  • Java 21 Sealed Hierarchies (Closed Algebraic Types):
    • Closed to adding new data types: The permits clause strictly enumerates all valid subtypes (sealed interface OrderStatus permits New, Paid, Cancelled).
    • Extremely easy to add new operations: Add a pattern-matching switch expression in an external service without altering the types.

Senior Verdict: Enforcing classical OCP polymorphism on inherently closed domain concepts (e.g., state machines, financial transaction statuses) is an architectural mistake. Sealed interfaces with pattern matching provide superior compile-time exhaustiveness and type safety.


4 Tricky Questions

1. How is the tension between OCP and YAGNI resolved in practice via the “Rule of Three”?

Answer: The Rule of Three establishes a pragmatic threshold:

  1. First occurrence: Implement the feature in the simplest, most direct manner possible (KISS, zero speculative abstractions).
  2. Second occurrence: Duplicate or adapt the logic, noting the shared structural pattern.
  3. Third occurrence: Only when the requirement manifests for the third time do you refactor toward OCP (extracting a polymorphic Strategy, Factory, or Interface). This protects the codebase from Speculative Generality and prevents maintaining dozens of single-implementation interfaces.

2. Is creating an interface with an Impl suffix (UserService -> UserServiceImpl) adhering to SOLID?

Answer: In $95\%$ of cases, this is the Header Interface Anti-Pattern—a mechanical cargo-cult practice that mimics SOLID without providing its benefits.

If an interface has exactly one implementation and is not crossing a remote architectural boundary or library boundary, it provides zero polymorphic extensibility (OCP) and inverts no dependencies (DIP). It merely doubles the file count and navigation friction in the IDE.

Senior Rule: Create interfaces only when at least two distinct implementations exist (e.g., PostgresUserRepository and InMemoryUserRepository), or when isolating an external infrastructure boundary.


3. Can you deliberately violate SOLID in JPA / Hibernate Entity classes?

Answer: Yes. JPA/Hibernate database entities deliberately violate multiple SOLID principles:

  • SRP Violation: Entities mix database schema definitions (@Column, @JoinColumn), validation annotations (@NotNull), persistence lifecycle callbacks (@PrePersist), and domain state.
  • Encapsulation Breach: Hibernate requires getters and setters or reflection access to populate instance state.

Attempting to split a JPA entity into pure SRP micro-classes destroys ORM relational mapping, causing severe query degradation (the $N+1$ query problem and runaway JOINs). Pragmatic architects accept this violation, keeping JPA entities strictly confined within the Data Access Layer (DAL) and mapping them to rich, encapsulated domain models at domain boundaries.


4. What is “Pain-Driven Development” (PDD) in the context of applying SOLID?

Answer: Pain-Driven Development is an engineering philosophy where architectural refactoring toward SOLID is executed reactively in response to real friction, rather than proactively based on theoretical speculation:

  • When writing unit tests becomes agonizing due to I/O dependencies $\implies$ introduce DIP (Constructor Injection of interfaces).
  • When changing a feature triggers unexpected breaks in unrelated modules $\implies$ decompose the class via SRP.
  • When multiple squads constantly encounter Git merge conflicts in the same if-else block $\implies$ refactor to OCP via the Strategy Pattern.

If a component functions reliably, has test coverage, and has not been modified in two years, leaving it alone is far more economically sound than refactoring it to satisfy theoretical SOLID purity.


🎯 Interview Cheat Sheet

30-Second Summary

  • The Core Answer: Theoretically yes; pragmatically in production, dogmatically applying all five principles everywhere causes severe Over-Engineering.
  • The Trade-Offs: SOLID improves maintainability and testability at the cost of increased structural complexity, higher memory footprint, and potential JIT devirtualization penalties.
  • When to Apply: Essential for core enterprise business logic (Core Domain) and shared libraries; harmful for throwaway prototypes, simple utilities, and ultra-low-latency CPU hot-paths.
  • Modern Alternative: Java 21 Sealed Interfaces with Pattern Matching replace classical OCP polymorphism for closed domain type hierarchies.

Common Follow-up Questions

  • “Which SOLID principle provides the highest ROI?” → Single Responsibility Principle (SRP). Keeping classes focused on a single business actor rarely adds unnecessary complexity and consistently improves testability and readability.
  • “What is the difference between good modularity and Atomic Dust?” → Good modularity groups related operations around cohesive domain boundaries (DDD aggregates); Atomic Dust fragments logic into hundreds of 3-line micro-classes that cannot function independently.

Red Flags (DO NOT Say)

  • ❌ “I always follow 100% of the SOLID principles on every class I write.” (Indicates dogmatism and a lack of real-world production trade-off experience).
  • ❌ “Interfaces and abstractions have zero performance cost in Java.” (Virtual method table lookups, memory alignment padding, and blocked inlining have measurable costs).
  • ❌ “Always create an interface for every service before writing the implementation.” (Creates the Header Interface anti-pattern).