Week 3 Day 4 — SOLID in a Small Backend
Goal
Today I want SOLID as five checks on the OrderService example, not as a poster on the wall.
Main questions:
- What does each letter mean in Java?
- What does a violation look like in a Spring service?
- Which letters do interviews actually probe?
- How far do I take this before it becomes busywork?
1. The five letters, in backend language
I keep one running example: placing an order.
S — Single Responsibility
A class should have one reason to change.
OrderService that saves the order, charges the card, sends email, and writes an audit file has four reasons to change. Split: OrderService orchestrates; PaymentGateway, OrderNotifier, AuditLog are collaborators.
Controllers stay thin. Repositories stay persistence. Mixing those layers is an SRP failure interviewers can see in a pull request.
O — Open/Closed
Open for extension, closed for modification.
New pricing: add a PricingPolicy implementation, not another if (vip) inside place(). I still edit code when the use case changes. Open/Closed is about stable callers when variants grow.
L — Liskov Substitution
Subtypes must honor the contract. Covered on Day 1. charge() that throws “not supported” is an LSP failure.
I — Interface Segregation
Many small capabilities beat one fat interface.
public interface OrderRepository {
Order save(Order order);
Optional<Order> findById(long id);
}
is better than an OrderOps that also sendWelcomeEmail and exportCsv. Implementors and fakes should not stub methods they do not own.
D — Dependency Inversion
High-level policy depends on abstractions, not on low-level details.
OrderService (policy) depends on PaymentGateway (abstraction). StripeGateway (detail) implements the abstraction. Spring’s constructor injection is how the inverted dependency is satisfied.
Memory sentence:
SOLID in Spring: one job per class, extend via new implementations, honor contracts, keep interfaces small, depend on abstractions.
2. A compact before / after
Before:
@Service
public class OrderService {
private final OrderJpaDao dao = new OrderJpaDao(); // constructs detail
public void place(OrderRequest req) {
if (req.vip) { /* inline discount */ }
dao.save(...);
new StripeClient().charge(...); // detail
JavaMailSenderImpl mail = new JavaMailSenderImpl();
mail.send(...);
}
}
After:
@Service
public class OrderService {
private final OrderRepository orders;
private final PaymentGateway payments;
private final PricingPolicy pricing;
private final OrderNotifier notifier;
public OrderService(
OrderRepository orders,
PaymentGateway payments,
PricingPolicy pricing,
OrderNotifier notifier) {
this.orders = orders;
this.payments = payments;
this.pricing = pricing;
this.notifier = notifier;
}
public Order place(CreateOrderRequest request) {
Money price = pricing.price(request);
Order order = orders.save(Order.from(request, price));
payments.charge(price);
notifier.placed(order);
return order;
}
}
S: orchestration only. O: new PricingPolicy. L: fakes honor contracts. I: each interface is one capability. D: constructor takes abstractions.
3. How far to take it
Interviews punish god classes and new StripeClient() inside the service. They also punish 15 interfaces for a three-field form.
Judgment:
- One concrete
@Servicewith no interface is fine while there is one implementation. - Extract an interface at the second implementation or the module boundary.
- Do not split
place()into six one-line classes to “look SOLID.” - Do split I/O, persistence, and HTTP out of domain policy.
Memory sentence:
SOLID is a smell detector. It is not a quota of interfaces.
4. What interviewers listen for
| Letter | Phrase they want |
|---|---|
| S | “The service orchestrates; email is another collaborator.” |
| O | “New variant = new implementation, not another if.” |
| L | “A fake must still fulfill charge.” |
| I | “I would not force a fake to implement exportCsv.” |
| D | “I depend on PaymentGateway; Spring injects Stripe.” |
If I only recite the Wikipedia line, I sound junior. If I walk through OrderService, I sound employable.
5. Spring extras that are really SOLID
@Primary/@Profile/@Conditional: choosing an implementation without editing the caller (O + D).@Transactionalon the service use case, not on the repository: one unit of work (S).- Controller → service → repository: each layer one job (S). Skipping the service for “it’s just CRUD” is a judgment call I can defend for tiny admin tools, not for anything with rules.
6. Common traps
Trap 1: Reciting SOLID without an example.
Trap 2: Interface for every class, including CreateOrderRequest.
DTOs are values. They are not dependencies.
Trap 3: Utils class with 40 static methods as “SRP because it is utilities.”
That is a junk drawer.
Trap 4: Breaking LSP with “optional” methods on a base type.
Practice Questions and Answers
Question 1
Explain Dependency Inversion with Spring.
Answer:
The high-level OrderService depends on a PaymentGateway interface. The low-level StripeGateway implements it. Spring injects the implementation into the constructor. The policy module does not new the detail.
Question 2
Give a Single Responsibility violation in a typical Boot app.
Answer:
A @RestController that contains SQL, payment calls, and email. Three reasons to change, hard to test, and @Transactional in the wrong place. Move rules to a service and I/O to collaborators.
Question 3
How does Open/Closed look without frameworks?
Answer:
A PricingPolicy interface and extra classes for extra policies. OrderService.place stays closed. I still change OrderService when the place-order use case itself changes.
Question 4
When is Interface Segregation violated?
Answer:
When a client depends on methods it does not use. A NotificationClient with sendEmail, sendSms, and sendPush forced on a test fake that only needs email. Split or use narrower types.
Question 5
Do I need five interfaces to be SOLID?
Answer:
No. I need classes that do not mix jobs, substitutions that honor contracts, and dependencies that point at abstractions where variants exist. A small app with one payment provider can keep one concrete class until the second appears.
Memory sentences
SOLID in Spring: one job per class, extend via implementations, honor contracts, small interfaces, depend on abstractions.
DI is Dependency Inversion made mechanical.
SOLID is a smell detector, not an interface quota.