Chaturmind
LearnDSASystem DesignInterview PrepDevOpsEngineering GrowthBlog
Start learning
Chaturmind

Structured learning paths for engineers who want to go deep. Written by practitioners.

Learn

  • Java
  • DSA
  • System Design
  • Spring Boot
  • AI / ML
  • DevOps
  • Engineering Growth
  • Java Interview Prep

Company

  • Blog
  • Contact

Legal

  • Privacy Policy
  • Terms of Service

© 2026 Chaturmind. All rights reserved.

Built for engineers who want to go deep.


← Java Interview Prep: 8+ Years (Senior & Lead)

Expert Core Java

  • Tricky Java Output, Operators & OOP Edge Cases — Interview Questions
  • Tricky Exceptions, Memory & Keyword Questions — Interview Questions
  • Classic Java Language Questions, Senior-Grade Answers — Interview Questions
  • Classic Collections, Threads & JDK APIs, Senior-Grade Answers — Interview Questions
  • Reflection, Dynamic Proxies, final & Modern OOP Design — Interview Questions

JVM Internals & Performance

  • Class Loading, Bytecode & Object Layout — Interview Questions
  • JIT Compilation & Runtime Optimisations — Interview Questions
  • Garbage Collectors Deep Dive — Interview Questions
  • JVM Tuning, GC Logs & Memory Footprint — Interview Questions
  • Memory Leaks, OutOfMemoryErrors & Profiling Tools — Interview Questions
  • Modules, Agents & Advanced JVM APIs — Interview Questions

Collections & Concurrency at Scale

  • Collections Internals & Complexity — Interview Questions
  • Iterators, Comparators & Ordering Contracts — Interview Questions
  • Concurrent Collections, Queues & Lock-Free Structures — Interview Questions
  • Threads, Executors & ForkJoin Internals — Interview Questions
  • Locks, Atomics, CAS & Synchronizers — Interview Questions
  • Java Memory Model, volatile, Fences & ThreadLocal — Interview Questions
  • Deadlock, Livelock, Starvation & Concurrent Design — Interview Questions
  • CompletableFuture, Parallel Streams & Non-Blocking I/O — Interview Questions

Modern Java (8 to 21+)

  • Lambdas & Functional Interfaces Internals — Interview Questions
  • Streams & Collectors Deep Dive — Interview Questions
  • Optional & Interface Default/Static Methods — Interview Questions
  • Java 9–25 Features & Virtual Threads — Interview Questions

Design Patterns, SOLID & Clean Code

  • Design Pattern Trade-offs & Combinations — Interview Questions
  • SOLID, Clean Code & Anti-Patterns — Interview Questions

Spring & Spring Boot Internals

  • IoC, Dependency Injection & Bean Lifecycle Internals — Interview Questions
  • Spring AOP, Proxies & @Async Internals — Interview Questions
  • Spring Configuration, Auto-Configuration & Custom Starters — Interview Questions
  • Spring MVC & REST Internals, Exception Frameworks — Interview Questions
  • Spring Security Advanced Internals — Interview Questions
  • Spring WebFlux, Reactor & R2DBC — Interview Questions
  • Spring Cloud, Observability & Distributed Tracing — Interview Questions
  • Spring Boot 3, Native Images & Production Scenarios — Interview Questions

JPA, Hibernate & Databases at Scale

  • Spring Data JPA — Queries, Projections, Custom Repositories & Locking — Interview Questions
  • JPA Entity Mapping, Associations & Cascades — Interview Questions
  • JPQL vs Native Queries in Depth — Interview Questions
  • Hibernate Caching — First-Level, Second-Level & Query Cache — Interview Questions
  • Lazy vs Eager Loading, LazyInitializationException & N+1 — Interview Questions
  • JPA Transactions, Propagation, Isolation & Dirty Checking — Interview Questions
  • SQL vs NoSQL, Indexing & Query Tuning — Interview Questions
  • Database Scaling, Replication, Pooling & Consistency Models — Interview Questions
  • Redis, Search, Time-Series, CDC & Transactional Data Modelling — Interview Questions

Testing Strategy & API Design

  • Spring Boot Test Slices, Context & Test Strategy — Interview Questions
  • Testing Web, Persistence, Security, Async & Messaging in Spring Boot — Interview Questions
  • JUnit 5 & Mockito, Advanced — Interview Questions
Chaturmind
← Java Interview Prep: 8+ Years (Senior & Lead)

Expert Core Java

  • Tricky Java Output, Operators & OOP Edge Cases — Interview Questions
  • Tricky Exceptions, Memory & Keyword Questions — Interview Questions
  • Classic Java Language Questions, Senior-Grade Answers — Interview Questions
  • Classic Collections, Threads & JDK APIs, Senior-Grade Answers — Interview Questions
  • Reflection, Dynamic Proxies, final & Modern OOP Design — Interview Questions

JVM Internals & Performance

  • Class Loading, Bytecode & Object Layout — Interview Questions
  • JIT Compilation & Runtime Optimisations — Interview Questions
  • Garbage Collectors Deep Dive — Interview Questions
  • JVM Tuning, GC Logs & Memory Footprint — Interview Questions
  • Memory Leaks, OutOfMemoryErrors & Profiling Tools — Interview Questions
  • Modules, Agents & Advanced JVM APIs — Interview Questions

Collections & Concurrency at Scale

  • Collections Internals & Complexity — Interview Questions
  • Iterators, Comparators & Ordering Contracts — Interview Questions
  • Concurrent Collections, Queues & Lock-Free Structures — Interview Questions
  • Threads, Executors & ForkJoin Internals — Interview Questions
  • Locks, Atomics, CAS & Synchronizers — Interview Questions
  • Java Memory Model, volatile, Fences & ThreadLocal — Interview Questions
  • Deadlock, Livelock, Starvation & Concurrent Design — Interview Questions
  • CompletableFuture, Parallel Streams & Non-Blocking I/O — Interview Questions

Modern Java (8 to 21+)

  • Lambdas & Functional Interfaces Internals — Interview Questions
  • Streams & Collectors Deep Dive — Interview Questions
  • Optional & Interface Default/Static Methods — Interview Questions
  • Java 9–25 Features & Virtual Threads — Interview Questions

Design Patterns, SOLID & Clean Code

  • Design Pattern Trade-offs & Combinations — Interview Questions
  • SOLID, Clean Code & Anti-Patterns — Interview Questions

Spring & Spring Boot Internals

  • IoC, Dependency Injection & Bean Lifecycle Internals — Interview Questions
  • Spring AOP, Proxies & @Async Internals — Interview Questions
  • Spring Configuration, Auto-Configuration & Custom Starters — Interview Questions
  • Spring MVC & REST Internals, Exception Frameworks — Interview Questions
  • Spring Security Advanced Internals — Interview Questions
  • Spring WebFlux, Reactor & R2DBC — Interview Questions
  • Spring Cloud, Observability & Distributed Tracing — Interview Questions
  • Spring Boot 3, Native Images & Production Scenarios — Interview Questions

JPA, Hibernate & Databases at Scale

  • Spring Data JPA — Queries, Projections, Custom Repositories & Locking — Interview Questions
  • JPA Entity Mapping, Associations & Cascades — Interview Questions
  • JPQL vs Native Queries in Depth — Interview Questions
  • Hibernate Caching — First-Level, Second-Level & Query Cache — Interview Questions
  • Lazy vs Eager Loading, LazyInitializationException & N+1 — Interview Questions
  • JPA Transactions, Propagation, Isolation & Dirty Checking — Interview Questions
  • SQL vs NoSQL, Indexing & Query Tuning — Interview Questions
  • Database Scaling, Replication, Pooling & Consistency Models — Interview Questions
  • Redis, Search, Time-Series, CDC & Transactional Data Modelling — Interview Questions

Testing Strategy & API Design

  • Spring Boot Test Slices, Context & Test Strategy — Interview Questions
  • Testing Web, Persistence, Security, Async & Messaging in Spring Boot — Interview Questions
  • JUnit 5 & Mockito, Advanced — Interview Questions
HomeLearnJava Interview PrepJava Interview Prep: 8+ Years (Senior & Lead)Design Patterns, SOLID & Clean Code
✓ FreeAdvanced· 13 min read

SOLID, Clean Code & Anti-Patterns — Interview Questions

Each SOLID principle with a Java example, SRP violations in small classes, OCP violations in switch statements, LSP and ISP through interfaces (with concrete violations), DIP violations and tight coupling, refactoring fat classes while keeping cohesion, misused DRY, premature abstraction and YAGNI, KISS vs DRY, how Clean Architecture enforces SOLID, code smells and fixes, enterprise Java anti-patterns, balancing performance with clean code, fluent API design, and signs a class or module does too much.

Published September 25, 2026


How to use this lesson

Principles questions are judged on concrete examples, and nuance. Every principle has a misuse mode:

  • SRP taken too far gives class explosion;
  • DRY taken too far gives coupling;
  • abstraction taken too far gives YAGNI.

Senior answers show you know when to apply a principle and when to bend it.

Q1. Explain each SOLID principle with a Java example.

Short answer:

  • S: Single Responsibility. A class should have one reason to change (one actor or stakeholder). InvoiceService computing totals and rendering PDFs and emailing them has three reasons to change. Split it into InvoiceCalculator, InvoicePdfRenderer and InvoiceMailer.
  • O: Open/Closed. Code should be open for extension, and closed for modification. Add new discount types as new DiscountPolicy implementations, instead of editing a switch.
  • L: Liskov Substitution. Subtypes must honour the base type's contract. A ReadOnlyList implements List whose add throws breaks callers who expect add to work. Model it as a separate read-only abstraction.
  • I: Interface Segregation. Clients shouldn't depend on methods they don't use. Split a fat Printer { print(); scan(); fax(); } into Printer, Scanner and Fax, so a simple printer doesn't implement fax() with a throw.
  • D: Dependency Inversion. High-level policy depends on abstractions, not on details. OrderService depends on a PaymentGateway interface (owned by the domain), with StripeGateway implementing it in the infrastructure layer, injected by constructor.

Learn it in depth → Object-Oriented Design Refresher

Q2. How can SRP be violated even by small classes?

Short answer: SRP is about reasons to change (the actors), not about size. A 40-line class can mix concerns:

  • a UserValidator that validates and writes audit logs and formats error messages for the UI;
  • a DTO with persistence annotations, JSON annotations, validation annotations and business logic, which changes with the database schema, the API contract and the business rules;
  • a small utility with static caches (a lifecycle concern) and formatting (a presentation concern);
  • a class that changes whenever two different teams change their requirements.

The test: "who asks for changes to this class?" More than one kind of stakeholder means more than one responsibility.

Q3. What's a real-world example of an OCP violation in a switch?

Short answer: A shipping-cost calculator that switches on the carrier:

BigDecimal cost(Shipment s) {
    switch (s.carrier()) {
        case "DHL":   return dhlRates(s);
        case "FEDEX": return fedexRates(s) .add(fuelSurcharge(s));
        case "BLUEDART": return blueDartRates(s);
        default: throw new IllegalArgumentException();
    }   // every new carrier edits this method, and every similar switch elsewhere (labels, tracking, returns)
}

The problem is that the same type-switch is duplicated in several places, so adding a carrier means editing all of them, each a regression risk. Fix: a Carrier strategy interface (cost, label, trackingUrl) with one implementation per carrier, registered in a map or injected as beans. New carriers are added, not edited in.

Key points to cover:

  • A counterpoint: with a sealed hierarchy and pattern-matching switches, a switch can be the right design, when the set of types is closed and operations are added more often than types (the expression problem). The compiler's exhaustiveness check makes it safe.

Q4. How do interfaces help with LSP and ISP? Give specific violations.

Short answer:

  • ISP: small, role-based interfaces mean implementations only promise what they can do. A violation: interface Repository<T> { save; delete; findAll; bulkImport; exportCsv; } forces a read-only reporting repository to throw on save/delete.
  • LSP: substitutability requires that implementations honour the contract: accept at least the same inputs, return at most the documented results, keep the invariants, and not throw unexpected exceptions. Violations:
    • Square extends Rectangle, where setWidth also changes the height, breaking callers' area expectations;
    • Collections.unmodifiableList() returns a List whose mutators throw. It's documented as optional, but it's a common practical LSP trap;
    • a subclass's withdraw() that rejects amounts the base class allowed (a strengthened precondition);
    • an overridden equals that breaks symmetry with the superclass.
  • Interfaces help by letting you model capabilities separately (Readable, Writable), so no type claims abilities it lacks. Sealed interfaces, and composition over inheritance, reduce accidental LSP breaks.

Learn it in depth → Liskov Substitution & Interface Segregation

Q5. Show how violating DIP leads to tightly coupled systems.

Short answer:

public class OrderService {
    private final MySqlOrderDao dao = new MySqlOrderDao();          // a concrete detail, created internally
    private final StripeClient stripe = new StripeClient("sk_live_..."); // a vendor SDK, and a secret, inside business logic
    public void place(Order o) { dao.insert(o); stripe.charge(o.total()); }
}

The consequences:

  • You can't unit-test without MySQL and Stripe.
  • Switching the database or payment provider means editing business code.
  • Vendor types leak into the domain.
  • Configuration and secrets are hard-coded.
  • Compile-time dependencies point from policy to details, so a change in the SDK recompiles or breaks the domain.

The fix: the domain defines OrderRepository and PaymentGateway interfaces. The infrastructure implements them (JpaOrderRepository, StripePaymentGateway). Dependencies are injected through the constructor. Now the arrows point inward, toward the domain.

Learn it in depth → Dependency Inversion

Q6. How would you refactor a fat class into smaller units, while keeping cohesion?

Short answer:

  1. Characterise it first: add tests around its current behaviour (characterisation or golden-master tests).
  2. Find the clusters: group methods by the fields they use and the reason they change (cohesion analysis; the LCOM metric; your IDE's dependency view).
  3. Extract classes along those seams: Extract Class, Move Method, and Introduce Parameter Object for data clumps. Name each class for its single role (PriceCalculator, StockReserver).
  4. Keep the original as a thin facade or coordinator during the transition, so callers don't all change at once. Then migrate the callers.
  5. Push behaviour into the domain objects that own the data ("tell, don't ask"), instead of creating anaemic helper classes.
  6. Avoid over-splitting: if two pieces always change together, they belong together. High cohesion within, low coupling between.

Q7. Why is DRY sometimes misused, leading to tight coupling?

Short answer: DRY is about knowledge (a single authoritative representation of a business rule), not about identical-looking code. The misuse is merging coincidentally similar code that serves different purposes. For example:

  • a shared validateAddress() used by billing and shipping, whose rules later diverge;
  • a common base class for unrelated services;
  • a shared DTO library across microservices.

The shared abstraction then couples independent parts. Every change needs coordination, and it grows flags and parameters (validate(address, isBilling, skipPostcode...)).

Heuristics:

  • the rule of three before abstracting;
  • ask whether the pieces would change for the same reason;
  • in microservices, prefer duplication over coupling across service boundaries: share contracts, not internal code.

Q8. Give an example of premature abstraction that violates YAGNI.

Short answer: Classic examples:

  • a PaymentProvider interface, factory, plugin loader and configuration system built for "future providers", when the product only integrates with one provider, and has no plans for more;
  • a generic "rules engine", with a DSL, for three simple if-statements;
  • a GenericRepository<T, ID, Q extends Query<?>> hierarchy wrapping Spring Data;
  • microservices for a 3-person team's MVP.

The costs: indirection that's hard to navigate, speculative flexibility that usually guesses wrong, and more code to maintain and test. YAGNI: build for today's requirements, keep the code easy to change, and refactor into an abstraction when the second real case arrives. (Extracting an interface later with IDE tooling is cheap.)

Q9. How does KISS differ from DRY? Can they conflict?

Short answer:

  • KISS (Keep It Simple): prefer the simplest design that works, and minimise cleverness and moving parts.
  • DRY (Don't Repeat Yourself): each piece of knowledge should have one authoritative source.

They conflict when removing duplication requires complex machinery: generic abstractions, reflection, metaprogramming, deep inheritance, or configuration-driven frameworks. Two simple, slightly duplicated methods may be easier to understand and change than one clever, parameterised method.

Resolve it by: duplicating simple, incidental code freely, and centralising business rules that must stay consistent. When in doubt, favour readability, because code is read far more often than it's written.

Q10. How does Clean Architecture enforce SOLID?

Short answer: Clean (Hexagonal, Onion) Architecture organises code in concentric layers: domain entities → use cases (application services) → interface adapters (controllers, repositories, gateways) → frameworks and drivers. The Dependency Rule says source-code dependencies point only inward.

  • DIP: use cases define ports (interfaces), and outer layers implement them as adapters. The domain never imports Spring, JPA or the HTTP layer.
  • SRP: each use case is a class with one reason to change. Adapters handle one technology each.
  • OCP: swap adapters (REST to gRPC, Postgres to Mongo) without touching the use cases.
  • ISP: ports are narrow and use-case-specific (LoadOrderPort, SaveOrderPort).
  • LSP: any adapter satisfying a port contract can be substituted, including test doubles.

Enforce it with module structure (Maven or Gradle modules, JPMS), and ArchUnit tests (for example, "domain classes must not depend on org.springframework.."). Beware the cost: too many layers and mapping code for simple CRUD services. Scale the architecture to the domain's complexity.

Q11. What is a code smell? Name three, and how you'd fix them.

Short answer: A surface symptom that suggests a deeper design problem. It's not a bug, but a warning sign. Three examples:

  1. Long method or god class: hard to understand, and touched by every change. Fix: Extract Method or Class, and split by responsibility.
  2. Feature envy: a method that uses another object's data more than its own (order.getCustomer().getAddress().getCity() chains). Fix: Move Method to the data owner. "Tell, don't ask."
  3. Primitive obsession: raw String/BigDecimal/long for emails, money and IDs, with validation scattered everywhere. Fix: value objects (records like Email, Money, OrderId) that validate in their constructors.

Others worth naming: shotgun surgery (one change touches many classes), duplicated switch statements, data clumps, long parameter lists (use a parameter object or builder), speculative generality, and comments explaining unclear code (rename or extract instead).

Q12. What design anti-patterns have you seen in enterprise Java code?

Short answer:

  • Anaemic domain model: entities with only getters and setters, and all the logic in huge *Service classes, so invariants aren't enforced.
  • God services or "manager" classes: thousands of lines, dozens of injected dependencies.
  • Leaking JPA entities through REST APIs: lazy-loading exceptions, over-fetching, mass-assignment risk, and API coupled to the schema.
  • The @Transactional everywhere cargo cult, or transactions around remote calls.
  • Exception swallowing (catch (Exception e) {}), and logging plus rethrowing at every layer.
  • Singletons with mutable static state, and service locators instead of dependency injection.
  • A distributed monolith: microservices that share a database, or must be deployed together.
  • Copy-paste inheritance hierarchies, deep Abstract* chains.
  • Over-engineering: unnecessary factories, interfaces with a single implementation "just in case", and XML or annotation magic.
  • N+1 queries, and business logic in stored procedures without tests.

For each, you'd describe the fix you applied: DTOs and mappers, rich domain objects, bounded contexts, outbox and events.

Q13. How do you balance performance optimisation with clean-code principles?

Short answer:

  • Clean by default: readable, simple and well-structured code is easier to optimise later, and most code isn't on the hot path.
  • Measure before optimising: use profiling (JFR, async-profiler), and benchmarks (JMH), to find the actual hot spots. The Pareto principle applies: a small fraction of the code dominates runtime.
  • Optimise the big levers first: algorithms and data structures, I/O (N+1 queries, batching, caching), and concurrency. They rarely hurt readability.
  • Contain the tricky optimisations: encapsulate low-level code (primitive arrays, object reuse, unrolled loops) behind clean interfaces, with comments explaining why, and benchmarks and tests guarding them.
  • Revisit them: re-measure after JDK upgrades. Old micro-optimisations can become pointless, or even harmful.

Q14. Why is fluent API design important in builders and DSLs? Give examples.

Short answer: Fluent APIs (method chaining that returns this or a next-step type) read like sentences in the domain language. They make configuration self-documenting, guide users through valid sequences (staged builders, where the type system enforces the order), and reduce errors from positional arguments. Examples:

  • HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(3)).followRedirects(NORMAL).build();
  • Stream pipelines;
  • Spring Security's HttpSecurity lambda DSL;
  • AssertJ (assertThat(order.total()).isPositive().isLessThan(limit));
  • jOOQ/QueryDSL/Criteria query builders;
  • Comparator.comparing(...).thenComparing(...);
  • WebClient/RestClient request builders.

Design tips:

  • Immutable intermediate steps (or clearly mutable builders).
  • Terminal operations that validate.
  • Discoverable naming, in the IDE.
  • Avoid chains that hide expensive operations, or make debugging hard.

Q15. What are the signs that a class or module is doing "too much"? How do you fix it?

Short answer:

  • The signs:
    • it's large, with many unrelated methods;
    • many dependencies injected (more than about 5–7 constructor parameters);
    • low cohesion (method groups using disjoint fields);
    • its name is vague ("Manager", "Helper", "Utils", "Processor");
    • frequent merge conflicts, and it changes for many different reasons;
    • its tests need huge setups, with many mocks;
    • it changes in almost every pull request.
  • For modules: cyclic dependencies, "shared" or "common" modules everyone depends on, and teams blocked on each other.
  • The fixes:
    • split along responsibilities or bounded contexts;
    • extract collaborators (and value objects);
    • introduce facades or ports for external callers;
    • move behaviour to the domain objects;
    • apply the Strangler approach for large modules;
    • enforce the new boundaries with ArchUnit or modules.

Follow-up questions this topic invites — and their answers

Q: Is SOLID still relevant with functional programming and records? A: Yes, in spirit. Functions and records reduce the need for some patterns, but single responsibility, depending on abstractions (function types are abstractions), and interface segregation (small functional interfaces) still apply.

Q: What is "tell, don't ask"? A: Instead of querying an object's state and deciding outside it (if (account.getBalance() >= x) account.setBalance(...)), tell the object what to do (account.withdraw(x)), so the logic and its invariants live with the data.

Q: How do you get a team to adopt clean-code practices without dogma? A: Agree on a small set of principles, with examples. Automate the style (formatters, linters, Sonar gates, ArchUnit). Use code reviews for design discussions, not nitpicks. Refactor opportunistically ("leave it better than you found it"), and measure outcomes (defect rates, lead time), not rule compliance.

Q: What is the Law of Demeter? A: "Only talk to your immediate friends". A method should call methods on itself, its fields, its parameters, and objects it creates, not on objects returned by other calls (a.getB().getC().doX()). It reduces coupling to object structure. Fluent builders and streams are acceptable exceptions.

Previous

Design Pattern Trade-offs & Combinations — Interview Questions

Next

IoC, Dependency Injection & Bean Lifecycle Internals — Interview Questions

AI Tutor

Lesson: SOLID, Clean Code & Anti-Patterns — Interview Questions

Quick actions

AI responses can be inaccurate. Verify critical information.