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.
🟢 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
-
Mistake: Creating
MathInterfaceandMathImplfor static utilities. Solution:Math.abs()violates DIP — and that’s fine. Don’t abstract what won’t change. -
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).
-
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
-
Static Methods and Utilities:
Math.abs(),Objects.requireNonNull(),StringUtils.isBlank()— violate DIP and SRP, but are useful. Don’t createMathInterfaceandMathImpl. Solution: Accept that utility classes are an acceptable SOLID violation. They are stable and domain-independent. -
Framework Constraints: Spring and Hibernate force SOLID violations. An Entity with 50 fields — an SRP violation.
@Autowiredfields — a DIP violation (dependency on the framework). Solution: Isolate violations in the Data Access Layer. Business logic shouldn’t know about frameworks. -
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. -
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-elsebranches — 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
finalclasses andstaticmethods — JIT guarantees inlining. - Layered SOLID: Full SOLID at boundaries (APIs, integrations), simplified inside hot paths.
Future Trends
- 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]]