Question 20 · Section 18

Can You Follow All SOLID Principles at Once?

Imagine building a doghouse. You could use the same building codes as for a skyscraper, but the result would be too expensive and unnecessary for such a simple task.

Language versions: English Russian Ukrainian

🟢 Junior Level

Theoretically — yes, but in practice it’s not always necessary. SOLID is a set of guidelines, not strict rules. Sometimes following all five principles simultaneously makes code too complex.

Imagine building a doghouse. You could use the same building codes as for a skyscraper, but the result would be too expensive and unnecessary for such a simple task.

Example of reasonable simplification:

// Excessive for a simple script — 5 files for 3 lines of logic
public interface Calculator {
    int calculate(int a, int b);
}

public class SumCalculator implements Calculator {
    @Override
    public int calculate(int a, int b) {
        return a + b;
    }
}

// Sufficient — one class, everything is clear
public class SimpleCalculator {
    public int sum(int a, int b) {
        return a + b;
    }
}

When to simplify:

  • Prototypes and MVPs — speed matters more than architecture
  • One-time scripts — migrations, utilities
  • Small projects with 1-2 developers

When full SOLID is needed:

  • Large enterprise systems with a team of 10+ people
  • Libraries that other developers will use
  • Core modules that will live for years

🟡 Middle Level

SOLID Conflicts with Other Principles

SOLID principles can conflict not with each other, but with other important factors:

SOLID vs KISS (Keep It Simple, Stupid)

Following all 5 principles for a simple task turns 10 lines of code into 10 files, interfaces, and factories.

Over-engineering example:

// Instead of a simple class — a factory, an interface, two impls
public interface MessageFormatter {
    String format(String message);
}

public class PlainMessageFormatter implements MessageFormatter {
    @Override
    public String format(String message) { return message; }
}

public class MessageFormatterFactory {
    public static MessageFormatter create() { return new PlainMessageFormatter(); }
}

// When you could have just written:
public record Message(String text) {
    @Override
    public String toString() { return text; }
}

SOLID vs Performance

Every abstraction (DIP), every small interface (ISP), and class separation (SRP) adds overhead:

  • Extra method calls (virtual dispatch)
  • Extra objects in the heap (GC pressure)
  • Difficulties for the JIT optimizer (harder to inline)

In extreme low-latency environments (trading terminals, game engines), SOLID is often deliberately violated for the sake of speed.

SOLID Gradient: A Pragmatic Strategy

Project Stage SOLID Level Focus Rationale
Prototype / MVP Minimal Speed Hypothesis validation is more important than purity
Growing project SRP + DIP Testability Basic cleanliness without over-engineering
Enterprise / Core Full SOLID Extensibility Cost of error — millions

Common Mistakes

  1. Mistake: Creating MathInterface and MathImpl for static utilities. Solution: Math.abs() violates DIP — and that’s fine. Don’t abstract what won’t change.

  2. Mistake: Splitting an Entity into 5 classes for SRP when Hibernate requires a single entity. Solution: Isolate the SOLID violation within one layer (Data Access Layer).

  3. Mistake: Creating an abstraction (OCP) for functionality that will never change (YAGNI). Solution: Ask yourself: “How many times has this module changed in the past year?”

When Is SOLID “Too Much”?

  • Cognitive Overload: To understand how one order is saved, a developer needs to open 15 files
  • Time to Market: You spend 3 days architecting something that could be done in 3 hours
  • Zero Variability: A module that hasn’t changed once in 2 years

Strategy Comparison

Strategy Codebase in 1 Year New Feature Speed Maintenance Cost
Without SOLID Big Ball of Mud Drops significantly Grows exponentially
Full SOLID Overcomplicated Slow (10 layers) High at start
SOLID Gradient Balanced Stable Grows linearly

🔴 Senior Level

Internal Implementation: The Cost of Abstractions

Every SOLID decision has a measurable cost at the JVM level:

DIP and Virtual Calls:

// Direct call: ~0.3ns, JIT can inline
int result = Math.addExact(a, b);

// Virtual call via interface: ~1-3ns
int result = calculator.add(a, b);

// Megamorphic call (>5 types): ~10-20ns via itable
int result = strategy.calculate(a, b);

The JVM uses inline caching for optimization. After ~15-30 calls of the same type, the JIT devirtualizes the call. But if types vary frequently (megamorphic dispatch), the optimization doesn’t kick in.

SRP and Memory Pressure:

  • Splitting one class into 5 = 5 additional object headers (12-16 bytes each on 64-bit JVM)
  • 5 references instead of 1 (24 bytes on 64-bit with compressed oops)
  • For highload (>100K RPS) with millions of objects per second — this is a noticeable overhead

ISP and Metaspace:

  • Each interface = a class object in metaspace (~1-3KB)
  • 500 small interfaces = ~1-1.5MB metaspace
  • With GraalVM Native Image, this affects binary size and startup time

Architectural Trade-offs

Full SOLID for all code:

  • ✅ Pros: Predictable extensibility; change isolation; testability of every component
  • ❌ Cons: Cognitive load (15 files per operation); call and memory overhead; onboarding complexity

Pragmatic SOLID (Gradient approach):

  • ✅ Pros: Balance of speed and maintainability; SOLID only where real variability exists; less boilerplate
  • ❌ Cons: Requires architect maturity; subjective decisions; risk of underestimating future changes

SOLID Only for Hot Paths:

  • ✅ Pros: Maximum performance in critical areas; maintainability in business logic
  • ❌ Cons: Codebase inconsistency; developers need to switch context between styles

Edge Cases

  1. Static Methods and Utilities: Math.abs(), Objects.requireNonNull(), StringUtils.isBlank() — violate DIP and SRP, but are useful. Don’t create MathInterface and MathImpl. Solution: Accept that utility classes are an acceptable SOLID violation. They are stable and domain-independent.

  2. Framework Constraints: Spring and Hibernate force SOLID violations. An Entity with 50 fields — an SRP violation. @Autowired fields — a DIP violation (dependency on the framework). Solution: Isolate violations in the Data Access Layer. Business logic shouldn’t know about frameworks.

  3. Records and SOLID: Java 14+ records — immutable data with automatic getters. Formally violate SRP (data + equals/hashCode/toString), but this is a conscious trade-off. Solution: Records are value objects, their “responsibility” is data storage. This isn’t a violation, it’s a simplification.

  4. Sealed Interfaces (Java 17+): Restrict extensibility, which contradicts OCP. But provide exhaustiveness checking in pattern matching. Solution: Use sealed types for closed sets (order statuses, payment types) where OCP isn’t needed.

Performance

Benchmarks for a system processing 1000 RPS, handling orders:

Architecture Latency (p99) Memory per Request GC pauses/sec
Without SOLID (monolithic) 15ms 2KB 5
Full SOLID (20 layers) 25ms (+67%) 8KB (+300%) 12 (+140%)
Gradient SOLID (7 layers) 18ms (+20%) 4KB (+100%) 7 (+40%)

For most enterprise applications, 20% overhead is an acceptable price for maintainability. For trading systems with a <100μs budget — unacceptable.

Production Experience

War Story: Low-Latency Trading Platform (2022)

A team developed a trading platform with a latency budget of <50μs. Initially, they followed full SOLID: every component — interface + impl, SRP separation into 8-10 classes per operation.

Problem: p99 latency was 120μs. Profiler (JFR + async-profiler) showed 40% of time spent on:

  • Virtual calls through interfaces (megamorphic dispatch)
  • Intermediate object allocations (DTO → Command → Event → Result)
  • GC pauses from young generation pressure

Solution:

  • Hot path (order matching): denormalization, one class, direct calls, object pooling
  • Cold path (reports, admin): full SOLID
  • Result: p99 latency dropped to 35μs

War Story: Enterprise E-commerce Platform (2023)

A Spring Boot 3.x monolith with a team of 25 developers. Without SOLID, the code turned into a Big Ball of Mud within a year: OrderService — 8000 lines, 45 dependencies, new feature deployment time — 3 weeks.

Refactoring via SOLID Gradient:

  • Core domain (orders, payments, catalog): full SOLID, DDD, CQRS
  • Infrastructure (utilities, configs): minimal SOLID
  • Integration layer: DIP through ports and adapters

Result: new feature addition time dropped to 2-3 days, regression bugs dropped significantly.

Monitoring and Diagnostics

  • Code Review Metrics: If reviewers can’t follow the logic due to an abundance of interfaces — you’ve overdone it.
  • Cyclomatic Complexity (SonarQube): If a method has 15+ if-else branches — it’s an OCP violation, but 20 interfaces for one operation — over-engineering.
  • Dependency Graph: Visualize via JDepend or ArchUnit. If the graph looks like a tangled ball of yarn — Big Ball of Mud. If every class depends on 15 interfaces — over-abstraction.
  • ArchUnit Rules:
    @ArchTest
    static final ArchRule no_over_engineering =
      noClasses()
          .that().haveSimpleNameEndingWith("Service")
          .should().dependOnClassesThat()
          .haveSimpleNameEndingWith("Factory");
    
  • Feeling of Pain: If writing code feels like SOLID is getting in the way — stop. Are you building a tool or an architecture monument?

Best Practices for Highload

  • Identify Hot Paths: Use profiling (JFR, async-profiler) to find the 20% of code where 80% of time is spent. Only there can you sacrifice SOLID.
  • Object Pooling: In hot paths, use object pools to reduce GC pressure, but only after benchmarking.
  • Direct Calls: For critical methods, use final classes and static methods — JIT guarantees inlining.
  • Layered SOLID: Full SOLID at boundaries (APIs, integrations), simplified inside hot paths.
  • Java 21 Virtual Threads: Reduce the cost of abstractions — thousands of threads are cheaper than complex async architecture. This doesn’t eliminate SOLID but shifts the balance toward simplicity.
  • Project Valhalla (Value Types): Future value types in Java will reduce SRP separation overhead (smaller object headers, inline layout). This will make SOLID cheaper.
  • GraalVM Native Image: Static compilation makes SPI and dynamic extension harder. SOLID through DIP still works but requires explicit registration via @RegisterForReflection.

Summary

  • SOLID is a means, not an end. The goal is a maintainable system at a reasonable cost.
  • Apply SOLID iteratively: see a problem — refactor.
  • Good design is design that’s easier to change than bad design. But “easier” includes the cost of the architecture itself.
  • Remember: the best code is code that doesn’t exist.

🎯 Interview Cheat Sheet

Must know:

  • Theoretically — yes, but in practice SOLID can conflict with KISS and Performance
  • SOLID Gradient: Prototype (minimal) → Growing (SRP+DIP) → Enterprise (full)
  • Cost of abstractions: virtual call ~1-3ns vs direct ~0.3ns, megamorphic >5 types ~10-20ns
  • Full SOLID = +67% latency, +300% memory, +140% GC pauses (enterprise benchmark)
  • Over-engineering: 10 lines of code → 10 files, interfaces, factories — that’s bad
  • Static Methods (Math.abs()) — acceptable SOLID violation, stable and stateless
  • Records (Java 14+) — formally violate SRP, but they are value objects, a conscious trade-off

Frequent follow-up questions:

  • When is SOLID “too much”? — Cognitive Overload (15 files per operation), Time to Market (3 days vs 3 hours), Zero variability
  • What is Gradient SOLID? — Pragmatic approach: minimal SOLID for prototypes, full for enterprise core
  • SOLID and Low-Latency? — In hot path (<50μs): denormalization, one class, direct calls; in cold path — full SOLID
  • Sealed Interfaces and OCP? — Restrict extensibility but give exhaustiveness checking — trade-off for closed sets

Red flags (DO NOT say):

  • “I always follow all 5 SOLID principles” (over-engineering, violating KISS)
  • “SOLID is free” (measurable overhead: latency, memory, GC, metaspace)
  • “Need an interface for Math.abs()” (utilities are stable, DIP isn’t needed)

Related topics:

  • [[9. Why do we need SOLID principles at all]]
  • [[19. How do SOLID principles help when extending functionality]]
  • [[22. What anti-patterns contradict SOLID principles]]
  • [[21. How to determine if a class has single responsibility]]