SOLID Principles Java Interview Questions 2026: 40+ Q&A for Senior Engineers
SOLID principles are tested in almost every senior Java interview. This guide gives you 40+ Q&A grouped by principle — SRP, OCP, LSP, ISP, and DIP — with real Spring Boot code examples and the exact answers that impress interviewers. Whether you are preparing for a FAANG-style design interview or a senior engineer panel, this is your definitive 2026 prep resource.
Table of Contents
- Why Interviewers Ask SOLID Questions
- SRP — Single Responsibility Principle Q&A
- OCP — Open/Closed Principle Q&A
- LSP — Liskov Substitution Principle Q&A
- ISP — Interface Segregation Principle Q&A
- DIP — Dependency Inversion Principle Q&A
- Cross-Principle & System Design Questions
- SOLID Study Checklist & Common Mistakes
1. Why Interviewers Ask SOLID Questions
SOLID principles are a proxy signal for how a candidate thinks about code quality, long-term maintainability, and team collaboration. When an interviewer asks "Can you give me an example of the Open/Closed Principle?", they are not looking for a textbook recitation — they want to hear whether you have encountered the pain of violating it in production, and how you reasoned your way to a better design.
At the senior level, SOLID questions evaluate three specific competencies: (1) design thinking — can you identify where a system will break down under change?; (2) communication — can you explain a design trade-off to a non-technical stakeholder?; and (3) pragmatism — do you know when to apply a principle and, just as importantly, when not to?
What Interviewers REALLY Want to Hear
- A concrete production story: "I refactored our payment service because adding a new provider required modifying three switch statements..."
- Trade-offs: "We introduced the Strategy pattern here, but for the reporting module we kept a simple if/else because it had only two branches and we didn't expect it to grow."
- Measurable impact: "Test coverage went from 40% to 82% after we split the God Service into focused collaborators."
3 Tips for Answering SOLID Questions
- Always anchor with a real example. Abstract definitions alone score 3/10. Real-world examples with code context score 9/10.
- Show both the violation and the fix. "Here's the before, here's why it hurt us, here's the refactored version" is the gold-standard answer structure.
- Acknowledge the cost. Every abstraction has overhead. Mentioning when NOT to apply a principle signals senior-level judgment.
2. SRP — Single Responsibility Principle Q&A
The Single Responsibility Principle states that a class should have one — and only one — reason to change. In Spring Boot services, SRP violations most commonly appear as services that mix orchestration, validation, notification, and persistence logic.
Q: What is SRP? How do you identify a violation?
A: SRP (Single Responsibility Principle) states that a class should have exactly one reason to change — meaning it should serve one business actor or one axis of change. To identify a violation, ask: "If I describe what this class does, do I need to use the word 'and'?" If a UserService "validates user input and sends welcome emails and updates analytics," it has three responsibilities. A practical second signal: if your unit test for this class requires mocking more than four or five collaborators, the class likely has too many responsibilities.
Q: Give a real Spring Boot example where you applied SRP.
A: In a past project, our OrderService handled payment validation, inventory decrement, email notification, and audit logging — all in one method. When the email provider changed their API, we had to modify and redeploy the entire order flow. We refactored into PaymentValidator, InventoryService, EmailNotificationService, and AuditService. OrderService became a pure orchestrator with four injected collaborators. Subsequent email provider changes required zero changes to order logic.
Q: Isn't one class = one method? How do you decide what "single responsibility" means?
A: No — one class per method is far too granular and produces unmaintainable micro-classes. The correct granularity is "one business actor." Robert Martin defines "responsibility" as "a reason to change," and a reason to change is tied to a stakeholder or requirement group. A UserRegistrationService that handles the full registration lifecycle (input validation, persistence, event publishing) is fine if those three concerns all belong to the same registration domain and change together. The split happens when a change in email template policy would force you to retest and redeploy order logic — different actors, different reasons.
Q: What is the difference between SRP and separation of concerns?
A: Separation of concerns (SoC) is the broader architectural principle — distinct concerns (UI, business logic, data access) should be handled by distinct modules. SRP is a class-level design principle — a single class should serve a single actor. SoC is satisfied at the layer level (controllers, services, repositories), while SRP is satisfied at the class level within those layers. You can have proper SoC and still violate SRP if your service layer has God Services.
Q: How does SRP relate to microservices?
A: SRP scales directly to the microservices level — a microservice should own a single business capability (the "bounded context" concept from DDD). An Order Service that also handles inventory, shipping, and notification is a macro-level SRP violation. Each capability should be independently deployable. In the same way that a God Class makes unit testing painful, a God Service makes independent deployment impossible. SRP is fractal: apply it at the method, class, module, and service level.
Q: UserService handles auth, profile management, and email sending. How do you refactor?
A: Extract three focused services and make UserFacadeService (or keep UserService) as a thin orchestrator:
// BEFORE: UserService doing too much
@Service
public class UserService {
public void register(UserRequest req) {
// Auth: hash password, create credentials
String hash = passwordEncoder.encode(req.getPassword());
userRepo.save(new User(req.getEmail(), hash));
// Profile: build profile record
profileRepo.save(new UserProfile(req.getName(), req.getAvatar()));
// Email: render and send welcome email
String body = templateEngine.render("welcome", req.getName());
mailClient.send(req.getEmail(), "Welcome!", body);
}
}
// AFTER: Three focused services + thin orchestrator
@Service
public class AuthService {
public Credentials register(String email, String rawPassword) {
return credentialsRepo.save(new Credentials(email,
passwordEncoder.encode(rawPassword)));
}
}
@Service
public class UserProfileService {
public UserProfile createProfile(UserRequest req) {
return profileRepo.save(new UserProfile(req.getName(), req.getAvatar()));
}
}
@Service
public class UserWelcomeEmailService {
public void sendWelcome(String email, String name) {
String body = templateEngine.render("welcome", name);
mailClient.send(email, "Welcome!", body);
}
}
@Service
@RequiredArgsConstructor
public class UserRegistrationService {
private final AuthService authService;
private final UserProfileService profileService;
private final UserWelcomeEmailService welcomeEmailService;
public void register(UserRequest req) {
authService.register(req.getEmail(), req.getPassword());
profileService.createProfile(req);
welcomeEmailService.sendWelcome(req.getEmail(), req.getName());
}
}Q: Can a Spring @Service violate SRP? Give an example.
A: Absolutely. The @Service annotation is just a stereotype marker — it does not enforce any design principle. A common violation is an AccountService that handles both the operational concern (CRUD for accounts) and the analytical concern (computing account metrics, generating statements, detecting fraud patterns). The fact that both belong to the "account" domain does not mean they have the same reason to change. Operational logic changes with business rules; analytical logic changes with reporting requirements. Two actors, two responsibilities, two classes.
Q: How do you test SRP compliance?
A: There is no automated tool that definitively measures SRP, but these practical checks work in code review: (1) count constructor arguments — more than five injected dependencies is a strong smell; (2) count lines of code per class — anything over 300 lines deserves scrutiny; (3) check the number of distinct @Mock annotations in the unit test — more than four mocks for a non-orchestrator service is suspicious; (4) ask if the class appears in git blame across unrelated feature branches — frequent multi-domain edits indicate multiple responsibilities. Tools like SonarQube's "Cognitive Complexity" and "Coupling Between Objects" metrics provide objective signals.
3. OCP — Open/Closed Principle Q&A
The Open/Closed Principle states that software entities should be open for extension but closed for modification. Achieving OCP means adding new behaviors without touching tested, deployed code.
Q: What is OCP and why is it hard to achieve?
A: OCP (Open/Closed Principle) states that a class should be open for extension — you can add new behavior — but closed for modification — you do not touch its existing, tested source code. It is hard to achieve because it requires anticipating axes of change before they occur. You must identify the "variation points" in your system (payment types, notification channels, discount rules) and protect them with abstractions. The challenge is that premature abstraction introduces complexity without benefit. Most practitioners apply OCP reactively: the first time you need a second variant, you introduce the interface; you do not pre-abstract for imagined futures.
Q: How do you extend behavior without modifying existing code in Spring Boot?
A: The primary mechanisms in Spring Boot are: (1) the Strategy pattern — define an interface, implement it as @Component, and let Spring collect all implementations via List<Strategy> injection; (2) Spring Events — publish an ApplicationEvent and add new @EventListener handlers without touching the publisher; (3) BeanPostProcessor / BeanFactoryPostProcessor — hook into the Spring lifecycle without modifying beans; (4) Decorator pattern — wrap existing beans with new behavior using @Primary on the wrapper. Each mechanism allows adding behavior through new code, not modified code.
Q: Show a concrete Java example of OCP violation and fix.
A:
// VIOLATION: Every new discount type requires modifying this class
@Service
public class DiscountService {
public double applyDiscount(Order order, String discountType) {
if ("SEASONAL".equals(discountType)) {
return order.getTotal() * 0.10;
} else if ("LOYALTY".equals(discountType)) {
return order.getTotal() * 0.15;
} else if ("FLASH_SALE".equals(discountType)) {
return order.getTotal() * 0.25;
}
return 0;
}
}
// FIX: Strategy interface — adding REFERRAL discount = new file, zero edits
public interface DiscountStrategy {
String type();
double calculate(Order order);
}
@Component
public class SeasonalDiscount implements DiscountStrategy {
@Override public String type() { return "SEASONAL"; }
@Override public double calculate(Order order) { return order.getTotal() * 0.10; }
}
@Component
public class LoyaltyDiscount implements DiscountStrategy {
@Override public String type() { return "LOYALTY"; }
@Override public double calculate(Order order) { return order.getTotal() * 0.15; }
}
@Service
public class DiscountService {
private final Map<String, DiscountStrategy> strategies;
public DiscountService(List<DiscountStrategy> list) {
this.strategies = list.stream()
.collect(Collectors.toMap(DiscountStrategy::type, s -> s));
}
public double applyDiscount(Order order, String type) {
return Optional.ofNullable(strategies.get(type))
.map(s -> s.calculate(order))
.orElse(0.0);
}
}Q: How does the Strategy pattern implement OCP?
A: The Strategy pattern is the canonical OCP implementation. The interface defines the contract (the "closed" part — the dispatcher never changes), while each implementation is a new concrete strategy (the "open" part — you add new behavior by adding new classes). The dispatcher is closed for modification because it only knows the interface; it is open for extension because any number of new strategies can be added as long as they implement the interface. In Spring, auto-discovery via @Component and list injection eliminates even the need to register strategies manually.
Q: Isn't changing interface implementations violating OCP?
A: No — OCP applies to the client code that depends on the abstraction, not to the implementation itself. If you are replacing an implementation (e.g., swapping an in-memory cache for Redis), you are extending the system's capabilities by providing a new implementation. The clients that depend on the CacheService interface do not change at all — that is OCP working correctly. The violation would be if you had to go into every service class that uses caching and change direct calls to a concrete InMemoryCacheService.
Q: How does Spring's BeanDefinitionRegistry relate to OCP?
A: Spring's BeanDefinitionRegistry and BeanFactoryPostProcessor are framework-level OCP mechanisms. They allow libraries and extensions to register additional beans or modify bean definitions without touching the application's core configuration code. For example, Spring Boot's auto-configuration adds beans based on classpath conditions — you add a starter dependency, and Spring Boot extends the application context without you modifying any existing @Configuration class. This is OCP at the framework level: the framework is open for extension (add a starter) but closed for modification (don't change SpringApplication).
Q: What is the "protected variation" principle and how does it relate to OCP?
A: "Protected variation" (a GRASP pattern by Craig Larman) states that you should identify points of predicted variation or instability and create a stable interface around them. It is the motivational force behind OCP — you protect stable code from the instability of changing details by wrapping the variation point in an abstraction. If you know payment types will grow, protect the payment dispatch code with a PaymentStrategy interface. If you know notification channels will grow, protect with a MessageSender interface. OCP is the principle; protected variation is the design heuristic that tells you where to apply it.
4. LSP — Liskov Substitution Principle Q&A
The Liskov Substitution Principle is the most subtle SOLID principle and the one most often explained poorly in interviews. Master it and you stand out immediately.
Q: What is LSP? State it formally.
A: Formally, Barbara Liskov stated: "If S is a subtype of T, then objects of type T may be replaced with objects of type S without altering any of the desirable properties of the program." In practical terms: any code that works with a base type must work correctly when given any subtype — without special-casing, without throwing unexpected exceptions, and without weakening guarantees. If a subclass overrides a method and changes its contract (weaker preconditions are OK; stronger postconditions, invariant violations, or new exceptions are not), it violates LSP.
Q: What is the classic Rectangle/Square violation? Show in Java.
A: The Rectangle/Square problem is LSP's most cited illustration. Mathematically, a square is a rectangle, so inheritance seems logical. But it breaks behavioral substitution:
// VIOLATION
public class Rectangle {
protected int width, height;
public void setWidth(int w) { this.width = w; }
public void setHeight(int h) { this.height = h; }
public int area() { return width * height; }
}
public class Square extends Rectangle {
@Override public void setWidth(int w) { this.width = this.height = w; }
@Override public void setHeight(int h) { this.width = this.height = h; }
}
// This test passes for Rectangle but FAILS for Square — LSP broken
void testArea(Rectangle r) {
r.setWidth(5);
r.setHeight(4);
assert r.area() == 20; // Square gives 16, not 20
}
// FIX: No inheritance — model shapes independently
public interface Shape {
int area();
}
public class Rectangle implements Shape {
private final int width, height;
public Rectangle(int w, int h) { this.width = w; this.height = h; }
@Override public int area() { return width * height; }
}
public class Square implements Shape {
private final int side;
public Square(int s) { this.side = s; }
@Override public int area() { return side * side; }
}Q: How do you detect LSP violations in code review?
A: Four red flags in code review: (1) a method override that throws UnsupportedOperationException — the subtype refuses to honour the supertype contract; (2) an override that adds a precondition that the parent did not have (e.g., requires non-null input when parent allowed null); (3) instanceof checks in polymorphic code — callers are special-casing subtypes instead of treating them uniformly; (4) test code that passes the parent type to a test method but the test fails when you substitute the subtype. A passing parent test suite that breaks when run against a subtype is the definitive LSP violation detector.
Q: Is interface-based LSP easier to satisfy than class inheritance?
A: Generally yes, because interfaces carry no implementation state, so there is no mutable state to violate. When a class implements an interface, the only contract to honour is the method signatures and their documented semantics. With class inheritance, you also inherit state and protected methods, which creates far more ways to break invariants. However, interface-based LSP violations still occur: an interface specifying void delete(UUID id) can be violated by an implementation that silently ignores the call (noop), or throws on certain IDs. The principle is about behavioural contracts, not syntactic signatures.
Q: Bird.fly() — should Penguin extend Bird? How to fix it?
A:
// VIOLATION: Penguin breaks code that calls fly() on any Bird
public class Bird {
public void fly() { System.out.println("Flying!"); }
}
public class Penguin extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException("Penguins cannot fly");
}
}
// Breaks at runtime:
List<Bird> flock = List.of(new Sparrow(), new Penguin());
flock.forEach(Bird::fly); // throws UnsupportedOperationException
// FIX: Model capability with interfaces
public interface Flyable { void fly(); }
public interface Swimmable { void swim(); }
public abstract class Bird {
public abstract void makeSound();
public abstract void eat();
}
public class Sparrow extends Bird implements Flyable {
@Override public void makeSound() { System.out.println("Tweet"); }
@Override public void eat() { /* seeds */ }
@Override public void fly() { System.out.println("Sparrow flying"); }
}
public class Penguin extends Bird implements Swimmable {
@Override public void makeSound() { System.out.println("Honk"); }
@Override public void eat() { /* fish */ }
@Override public void swim() { System.out.println("Penguin swimming"); }
}
// Safe — only flyable birds in this list
List<Flyable> flyers = List.of(new Sparrow(), new Eagle());
flyers.forEach(Flyable::fly);Q: How does LSP relate to Liskov's original definition (preconditions/postconditions)?
A: Liskov's formal definition uses Hoare-style contracts: a subtype method must accept at least as broad a set of inputs (preconditions can only be weakened or maintained, never strengthened) and must guarantee at least the same results (postconditions can only be strengthened, never weakened). Additionally, invariants of the parent type must be preserved. In practice: a save() method that accepts any non-null object in the parent must not require a validated object in the subtype (that strengthens preconditions). And if the parent guarantees the returned object is non-null, the subtype cannot return null (that weakens postconditions).
Q: Give a real microservices scenario where LSP was violated.
A: A common real-world scenario: a team defined a PaymentGateway interface with a refund(String txId, BigDecimal amount) method. Nine gateways implemented it fully. Then a new gateway was added for a restricted market that did not support refunds. The implementation threw RefundNotSupportedException on every call. All the order-management code that assumed any PaymentGateway could refund broke at runtime in that market. The fix was a capability interface: RefundableGateway extends PaymentGateway, and order management queries for RefundableGateway explicitly before attempting a refund. This makes the type system enforce the contract rather than relying on runtime exceptions.
5. ISP — Interface Segregation Principle Q&A
The Interface Segregation Principle states that no client should be forced to depend on methods it does not use. It is the interface-level equivalent of SRP.
Q: What is ISP? Provide a fat interface example.
A: ISP (Interface Segregation Principle) states that clients should not be forced to depend on interfaces they do not use. A "fat interface" is one that bundles many unrelated methods together, forcing every implementor to stub methods it does not need. Classic fat interface example:
// FAT INTERFACE — violates ISP
public interface ReportService {
// Read-only analytics consumers only need these:
List<Report> findByDateRange(LocalDate from, LocalDate to);
Report findById(UUID id);
long count();
// Admin-only operations — why should analytics consumers depend on these?
Report save(Report r);
void delete(UUID id);
void archive(UUID id);
void exportToCsv(UUID id, OutputStream out);
void sendByEmail(UUID id, String recipient);
}
// SEGREGATED — each client gets exactly what it needs
public interface ReportQueryService {
List<Report> findByDateRange(LocalDate from, LocalDate to);
Report findById(UUID id);
long count();
}
public interface ReportCommandService {
Report save(Report r);
void delete(UUID id);
void archive(UUID id);
}
public interface ReportExportService {
void exportToCsv(UUID id, OutputStream out);
void sendByEmail(UUID id, String recipient);
}Q: Worker interface with work(), eat(), and sleep() — why is this wrong?
A: This is the canonical ISP violation example. A Robot implementing Worker does not eat or sleep — it must provide no-op stubs or throw exceptions. A RemoteContractor working part-time does not sleep in the "company context." Forcing all worker types to implement all three methods creates stub pollution and makes the type system misleading. The correct design is three role interfaces: Workable (with work()), Feedable (with eat()), and Restable (with sleep()). A human Employee implements all three. A Robot implements only Workable.
Q: How does ISP relate to API design?
A: ISP is directly applicable to public REST API and SDK design. A "fat" API endpoint that returns a 50-field object when a mobile client only needs 5 fields violates ISP at the API level — clients are forced to depend on (receive, parse, and version against) data they do not need. The GraphQL "ask for exactly what you need" philosophy is essentially ISP applied to API design. In REST, solutions include field filtering (?fields=id,name,email), purpose-specific endpoints (GET /users/summary vs GET /users/{id} for full detail), and response shaping in BFF (Backend for Frontend) layers.
Q: In Spring Boot repositories, how does ISP apply?
A: Spring Data repositories are excellent ISP examples. Instead of exposing one repository interface with all CRUD + query methods to every service, you define role-specific interfaces. A read-only analytics service should inject a UserQueryRepository (extending Repository<User, UUID> with only query methods), not the full JpaRepository which exposes destructive delete and deleteAll. This also reduces the mock surface in tests — you mock only the methods you actually call. Spring Data allows fine-grained repository interfaces via @NoRepositoryBean:
@NoRepositoryBean
public interface UserQueryRepository extends Repository<User, UUID> {
Optional<User> findById(UUID id);
Optional<User> findByEmail(String email);
List<User> findByActiveTrue();
long countByActiveTrue();
}
// The JPA impl extends both:
public interface UserJpaRepository
extends UserQueryRepository, JpaRepository<User, UUID> {}
// Analytics only sees queries:
@Service
@RequiredArgsConstructor
public class UserAnalyticsService {
private final UserQueryRepository repo; // no delete/save visible
}Q: Difference between ISP and SRP?
A: SRP is about the class or module having only one reason to change — it is about what the class does. ISP is about the interface that clients depend on — it is about what the class exposes. A class can satisfy SRP (it has one responsibility) while still violating ISP (it exposes methods relevant to only a subset of its clients through one fat interface). Conversely, you can violate SRP by combining two responsibilities into one class, each individually satisfying ISP via separate interfaces. In practice, applying SRP tends to naturally help ISP (smaller classes = smaller interfaces), and applying ISP can reveal hidden SRP violations.
Q: How do Java 8 default methods affect ISP?
A: Java 8 default methods can both help and hurt ISP. They help by allowing you to add optional, convenience behaviours to an interface without forcing every implementor to add a method body — reducing the cost of extending an interface and making fat interfaces slightly less painful. However, they can be misused to pack utility methods into interfaces, creating fat interfaces justified by "at least they have default implementations." The correct use of default methods for ISP is to provide sensible no-op or exception-throwing defaults for optional capabilities, signalling to implementors that they only need to override methods relevant to their context.
6. DIP — Dependency Inversion Principle Q&A
The Dependency Inversion Principle is perhaps the most architecturally impactful SOLID principle, and Spring Boot is essentially a DIP engine.
Q: What is DIP? High-level vs low-level modules.
A: DIP has two rules: (1) High-level modules should not depend on low-level modules — both should depend on abstractions. (2) Abstractions should not depend on details; details should depend on abstractions. High-level modules contain business logic (e.g., OrderService). Low-level modules contain implementation details (e.g., SmtpEmailSender, MySQLOrderRepository). Without DIP, OrderService directly imports and instantiates SmtpEmailSender — a change in email infrastructure requires changes to business logic. With DIP, OrderService depends on a MessageSender interface. The SMTP implementation depends on the same interface. Business logic and infrastructure details are decoupled at the abstraction.
Q: How does Spring IoC implement DIP? (with code)
A: Spring's IoC container is a DIP implementation machine. You declare dependencies as interface types in constructors, and Spring resolves and injects the concrete implementation at startup:
// Abstraction — both sides depend on this
public interface NotificationPort {
void send(String recipient, String message);
}
// Low-level detail depends on the abstraction
@Service("emailNotification")
public class EmailNotificationAdapter implements NotificationPort {
private final JavaMailSender mailSender;
public EmailNotificationAdapter(JavaMailSender mailSender) {
this.mailSender = mailSender;
}
@Override
public void send(String recipient, String message) {
SimpleMailMessage mail = new SimpleMailMessage();
mail.setTo(recipient);
mail.setText(message);
mailSender.send(mail);
}
}
// High-level module depends ONLY on the abstraction — never on the detail
@Service
@RequiredArgsConstructor
public class OrderNotificationService {
private final NotificationPort notificationPort; // Spring injects EmailNotificationAdapter
public void notifyShipped(Order order) {
notificationPort.send(order.getCustomerEmail(),
"Your order " + order.getId() + " has shipped!");
}
}
// In tests — swap the real sender for a mock in milliseconds:
@ExtendWith(MockitoExtension.class)
class OrderNotificationServiceTest {
@Mock NotificationPort notificationPort;
@InjectMocks OrderNotificationService service;
@Test
void notifiesCustomerOnShipment() {
Order order = new Order("o-1", "user@example.com");
service.notifyShipped(order);
verify(notificationPort).send("user@example.com", "Your order o-1 has shipped!");
}
}Q: What is the difference between DIP and dependency injection?
A: DIP is the design principle — high-level and low-level modules should both depend on abstractions. Dependency injection (DI) is the mechanism (a design pattern) used to achieve DIP. You can have DI without DIP: if you inject a concrete class (@Autowired SmtpEmailSender emailSender), you are using DI but violating DIP because the high-level module still depends on the low-level detail. You can also have DIP without DI: if a class creates an implementation via a factory method that returns an interface type, that satisfies DIP without using a DI framework. In Spring Boot, constructor injection of interface types combines both: the mechanism (DI) implements the principle (DIP).
Q: OrderService directly instantiates EmailService — violation and fix.
A:
// VIOLATION: High-level OrderService hard-wired to low-level EmailService
@Service
public class OrderService {
// new keyword creates tight coupling — untestable, unswappable
private final EmailService emailService = new EmailService("smtp.company.com", 587);
public void placeOrder(Order order) {
orderRepo.save(order);
emailService.sendConfirmation(order.getUserEmail(), order.getId()); // cannot mock this!
}
}
// FIX: Introduce abstraction, inject via constructor
public interface OrderEmailPort {
void sendConfirmation(String email, UUID orderId);
}
@Component
public class SmtpOrderEmailAdapter implements OrderEmailPort {
private final JavaMailSender mailSender;
public SmtpOrderEmailAdapter(JavaMailSender mailSender) {
this.mailSender = mailSender;
}
@Override
public void sendConfirmation(String email, UUID orderId) {
// SMTP send implementation
}
}
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepo;
private final OrderEmailPort orderEmailPort; // interface, not concrete class
public void placeOrder(Order order) {
orderRepo.save(order);
orderEmailPort.sendConfirmation(order.getUserEmail(), order.getId());
// Now testable: @Mock OrderEmailPort orderEmailPort in test
}
}Q: How do you test DIP compliance?
A: The best DIP compliance test is the pure unit test: if you can write a unit test for a business service class with only @Mock (Mockito) dependencies and no @SpringBootTest, no real database, and no real network calls, you have achieved DIP. The test setup becomes the specification. Additionally, scan import statements: if a service in the application or domain package imports from the infrastructure or adapter package (e.g., import com.example.infrastructure.SmtpEmailSender), that is a DIP violation. Architecture enforcement tools like ArchUnit can automate this as part of CI:
// ArchUnit test to enforce DIP at CI
@AnalyzeClasses(packages = "com.example")
class DipArchitectureTest {
@ArchTest
static final ArchRule domainMustNotDependOnInfrastructure =
noClasses().that().resideInAPackage("..domain..")
.should().dependOnClassesThat()
.resideInAPackage("..infrastructure..");
}Q: What is an abstraction layer? When should you NOT add one?
A: An abstraction layer is an interface (or abstract class) that sits between a high-level consumer and a low-level implementation, decoupling the two from each other. You should not add one when: (1) there is only one implementation and no realistic prospect of a second — the interface adds indirection without flexibility; (2) the abstraction leaks implementation details (a JdbcUserRepository interface with executeQuery(String sql) is not really an abstraction); (3) you are writing a small internal script or prototype where testability is not a concern; (4) the cost of the abstraction exceeds the benefit (e.g., abstracting LocalDateTime.now() for a one-time batch job). The rule: abstract when there are at least two implementations, or when testing the consumer requires avoiding the implementation's side effects.
Q: How does DIP relate to the ports and adapters (hexagonal) architecture?
A: Hexagonal architecture (ports and adapters, by Alistair Cockburn) is DIP applied as a full architectural pattern. In hexagonal architecture, the domain core defines "ports" — interfaces representing capabilities it needs (e.g., SendNotificationPort, LoadOrderPort). "Adapters" are the low-level implementations that satisfy those ports (e.g., SmtpNotificationAdapter, JpaOrderAdapter). The core never depends on adapters; adapters depend on the ports defined by the core. This is exactly DIP: the business logic (high-level) and the infrastructure (low-level) both depend on the port abstraction, not on each other. Spring Boot maps naturally: @Service classes are core, @Repository and infrastructure @Component classes are adapters.
7. Cross-Principle & System Design Questions
Senior interviews often move from principle-specific questions to synthesis questions that test whether you can apply SOLID holistically across a system or team context.
Q: How do SOLID principles relate to clean code and design patterns?
A: SOLID principles are the design philosophy; design patterns are the implementation vocabulary; and clean code is the craftsmanship layer that binds them together. The Strategy pattern is the canonical OCP implementation. The Decorator pattern is an OCP + ISP implementation. Factory patterns support DIP by providing implementations without hard-wiring them in business code. Clean code practices (meaningful names, small functions, no comments needed because the code reads naturally) make SOLID compliance visible and maintainable. A codebase can satisfy all SOLID principles and still be unmaintainable through poor naming and inconsistent abstractions; clean code and SOLID together produce genuinely readable, extensible systems.
Q: If you had to pick the most important principle, which would you choose and why?
A: This is a great question because there is no single correct answer — what matters is the reasoning. My personal choice is SRP, because all other principles become much easier to apply once responsibilities are correctly separated. A class with a single responsibility is naturally small, making it easier to define a narrow interface (ISP), easier to substitute with a variant (LSP), easier to extend without modification (OCP), and easier to inject without coupling (DIP). SRP violations compound all other violations. That said, a reasonable argument can be made for DIP: without dependency inversion, testability suffers immediately and the entire test pyramid becomes an integration test pyramid — the consequences are felt every day. Both answers are acceptable; the reasoning is what the interviewer is evaluating.
Q: Give an example of refactoring a God Class using all 5 SOLID principles.
A: Consider a UserManagementService with 50 methods handling registration, profile update, password reset, role management, analytics, and audit logging. Refactoring with all five principles:
- SRP: Extract
UserRegistrationService,UserProfileService,PasswordResetService,RoleManagementService,UserAnalyticsService,AuditService— one reason to change each. - OCP: Model role assignment rules as a
RoleAssignmentStrategyinterface so new roles (e.g., "regional admin") can be added as new classes without modifying existing logic. - LSP: Ensure
AdminUserService extends UserServicedoes not override any method with a weaker guarantee or a stronger precondition. If admin operations add preconditions, model them as a separateAdminOperationsinterface. - ISP: Expose
UserQueryPort(for read-only consumers like analytics),UserCommandPort(for write operations), andAuditPort(for audit consumers). No consumer sees methods it doesn't need. - DIP: Each service depends on port interfaces, not concrete repositories or email clients. Infrastructure adapters implement the ports. Tests mock the ports in milliseconds.
Q: How do SOLID principles apply to microservices architecture?
A: SOLID principles scale from class to service level directly: SRP → each microservice owns one business capability (bounded context); OCP → publish domain events so consumers can extend behaviour without the producer knowing about them (event-driven extensibility); LSP → a new version of a service's API must be backward compatible — consumers should be substitutable without regression (semantic versioning + contract testing with Pact); ISP → use the BFF (Backend for Frontend) pattern to expose purpose-specific APIs instead of one large general-purpose API; DIP → services communicate via contracts (OpenAPI, Protobuf, AsyncAPI), not direct implementation coupling. Violating SOLID at the service level produces the same pathologies as at the class level, just at 10x the cost of remediation.
Q: Can SOLID be over-applied? What's the cost?
A: Absolutely — over-application of SOLID produces "Ravioli Code" or "Architecture Astronaut" syndrome. Signs of over-application: interfaces that have only one implementation and will never have a second; abstractions so thin they add no meaningful decoupling; five layers of indirection for a 10-line business rule; more interface files than implementation files. The real cost is cognitive overhead — new team members cannot trace a request through the codebase without jumping through six levels of indirection. The antidote is YAGNI (You Aren't Gonna Need It) and the "Rule of Three": introduce abstraction when you see a third instance of a pattern, not speculatively. SOLID principles are design guides, not laws that must be applied to every class.
Q: How do you introduce SOLID to a team with legacy code?
A: Three-phase approach: (1) Measure pain first — run SonarQube and identify the top five God Classes by coupling metrics and bug frequency. Show the team where design debt is costing the most time. (2) Apply the Boy Scout Rule — introduce SOLID incrementally: every time a developer touches a God Class for a feature, they extract one responsibility into a new class. No big-bang rewrites. (3) Codify as standards — add ArchUnit tests to enforce DIP boundaries in CI, add SonarQube quality gates for cognitive complexity and coupling, add code review checklist items for SOLID signals. The transition from reactive to proactive SOLID compliance typically takes 3-6 months for a medium-sized team with legacy debt.
8. SOLID Study Checklist & Common Mistakes
Use this checklist in the 48 hours before your interview to confirm you are ready for every SOLID question that can come up at the senior level.
✅ SOLID Interview Study Checklist (10 items)
- ☐ Can you state all five principles in one sentence each, from memory?
- ☐ Do you have a real production story (violation + fix + measurable outcome) for at least three principles?
- ☐ Can you write the before/after code for SRP (UserService refactor), OCP (Strategy pattern), and DIP (interface injection) on a whiteboard or shared editor?
- ☐ Can you explain the Rectangle/Square LSP violation and its interface-based fix?
- ☐ Can you draw the difference between DIP (principle) and dependency injection (mechanism)?
- ☐ Can you describe when NOT to apply each principle (the over-application cost)?
- ☐ Can you map each principle to a microservices-level concern (SRP → bounded context, OCP → events, LSP → API versioning, ISP → BFF, DIP → contract-first communication)?
- ☐ Do you know at least one tool that enforces each principle at CI level (ArchUnit for DIP, SonarQube coupling metrics for SRP/ISP, Pact for LSP in services)?
- ☐ Can you describe how to introduce SOLID to a legacy codebase without a big-bang rewrite?
- ☐ Have you reviewed the related posts below for SOLID in context with Clean Code, refactoring, and design patterns?
⚠️ Common Interview Mistakes
| Mistake | Why It Fails | Better Answer |
|---|---|---|
| Reciting textbook definitions only | Shows theoretical knowledge, no evidence of practical application | Follow every definition with a 30-second real-world story |
| Claiming SOLID is always beneficial | Ignores YAGNI and the cost of unnecessary abstraction; signals inexperience | Acknowledge when NOT to apply (CRUD services, single-implementation scenarios) |
| Confusing DIP with dependency injection | These are not the same; conflating them signals shallow understanding | Explain: DI is the mechanism, DIP is the principle; DI can still violate DIP if injecting concretes |
| Only discussing LSP with Bird/Penguin | Everyone uses this example; it suggests you learned from a tutorial, not production | Add a real microservices scenario: API versioning, payment gateway refund support, etc. |
| Treating ISP as just "small interfaces" | Misses the client-centric definition — it's about what clients depend on, not just interface size | Frame ISP around the consumer's perspective: "each client should see only the methods it needs" |
| Not connecting SOLID to testability | SOLID is most compelling when explained through its testing benefits; missing this is a missed opportunity | Always mention: SRP → fewer mocks per test; DIP → pure unit tests without infrastructure |
Leave a Comment
Related Posts
Software Engineer · Java · Spring Boot · Microservices