SOLID Principles in Practice — Cheat Sheet
Low Level Design · 5 topics. Download the PDF or the Instagram carousel and share it.
Cheat Sheet · AiCanCode.org
SOLID Principles in Practice
Low Level Design5 topicsQuick revision reference
1
Single Responsibility Principle
A class should have only one reason to change — it should do one thing and do it well.
- ✓A class should have one reason to change — one axis of variation, one responsibility.
- ✓God classes that do everything are the most common SRP violation.
- ✓SRP improves testability — small, focused classes are easier to unit test in isolation.
- ✓SRP applies at method level too — methods should do one thing, named as a verb-noun describing that one thing.
- ✓SRP and high cohesion are the same idea: group things that change together.
Java — SRP violation and refactoring
// ❌ SRP Violation: UserService does too many things
public class UserService {
public void registerUser(String email, String password) {
// 1. Validate
if (!email.contains("@")) throw new IllegalArgumentException("Invalid email");
// 2. Hash password
String hashed = BCrypt.hashpw(password, BCrypt.gensalt());
// 3. Save to database
String sql = "INSERT INTO users (email, password) VALUES (?, ?)";
jdbcTemplate.update(sql, email, hashed);
// 4. Send welcome email
MimeMessage msg = mailSender.createMimeMessage();
// ... email setup ...
mailSender.send(msg);
// 5. Generate PDF welcome packet
Document doc = new Document();
PdfWriter.getInstance(doc, new FileOutputStream("welcome.pdf"));
// ... PDF generation ...
}
// This class changes when: email template changes, PDF library upgrades,
// DB schema changes, validation rules change, or password policy changes
}
// ✅ SRP Refactoring: one class, one responsibility
public class UserRegistrationService { // orchestrates only
private final UserRepository userRepo;
private final PasswordEncoder encoder;
private final EmailService emailService;
private final WelcomeDocService docService;
public UserRegistrationService(UserRepository userRepo, PasswordEncoder encoder,
EmailService emailService, WelcomeDocService docService) {
this.userRepo = userRepo;
this.encoder = encoder;
this.emailService = emailService;
this.docService = docService;
}
public User registerUser(String email, String password) {
User user = User.of(email, encoder.encode(password));
userRepo.save(user);
emailService.sendWelcome(user);
docService.generateWelcomePacket(user);
return user;
}
}
// Each extracted class has one reason to change:
public class EmailService {
public void sendWelcome(User user) { /* only email logic here */ }
}
public class WelcomeDocService {
public void generateWelcomePacket(User user) { /* only PDF logic here */ }
}
public class UserRepository {
public void save(User user) { /* only DB logic here */ }
}2
Open/Closed Principle
Software entities should be open for extension but closed for modification — add new behavior by writing new code, not changing existing code.
- ✓Open for extension = add new behavior by writing new classes/methods.
- ✓Closed for modification = existing tested code is not changed.
- ✓Strategy and Template Method are the primary OCP enablers in Java.
- ✓The key is identifying the right extension point — abstract over what varies.
- ✓100% OCP is impossible — aim to protect the most volatile variation points.
Java — OCP with Strategy (Discount Calculator)
// ❌ OCP Violation: modify this class every time a new discount type is added
public class DiscountCalculator {
public double calculate(Order order) {
double discount = 0;
if (order.getType() == OrderType.SEASONAL) {
discount = order.getTotal() * 0.10;
} else if (order.getType() == OrderType.EMPLOYEE) {
discount = order.getTotal() * 0.20;
} else if (order.getType() == OrderType.BULK) {
discount = order.getQuantity() > 10 ? order.getTotal() * 0.15 : 0;
}
// Adding a new type requires editing this class — OCP violation
return discount;
}
}
// ✅ OCP with Strategy pattern
@FunctionalInterface
public interface DiscountStrategy {
double calculate(Order order);
}
// Each discount type is a new class — no modification needed
public class SeasonalDiscount implements DiscountStrategy {
@Override public double calculate(Order o) { return o.getTotal() * 0.10; }
}
public class EmployeeDiscount implements DiscountStrategy {
@Override public double calculate(Order o) { return o.getTotal() * 0.20; }
}
public class BulkDiscount implements DiscountStrategy {
@Override
public double calculate(Order o) {
return o.getQuantity() > 10 ? o.getTotal() * 0.15 : 0;
}
}
// Closed for modification — never needs to change for new discount types
public class DiscountCalculator {
private final List<DiscountStrategy> strategies;
public DiscountCalculator(List<DiscountStrategy> strategies) {
this.strategies = strategies;
}
public double calculate(Order order) {
return strategies.stream()
.mapToDouble(s -> s.calculate(order))
.sum();
}
}
// Adding a loyalty discount: create a new class, register it — zero modification
public class LoyaltyDiscount implements DiscountStrategy {
@Override public double calculate(Order o) {
return o.getLoyaltyPoints() > 1000 ? o.getTotal() * 0.05 : 0;
}
}3
Liskov Substitution Principle
Subtypes must be substitutable for their base types without altering the correctness of the program.
- ✓Subtypes must be substitutable for their base type without breaking program correctness.
- ✓Square-Rectangle is the canonical LSP violation — Square breaks Rectangle's independent-dimensions invariant.
- ✓Subclasses may only weaken preconditions (accept more) and strengthen postconditions (return more).
- ✓Throwing new unchecked exceptions for valid parent inputs violates LSP.
- ✓instanceof checks in client code are a red flag — they indicate the caller knows about subtypes, signaling an LSP problem.
Java — LSP violation (Square-Rectangle) and fix
// ❌ Classic LSP violation
public class Rectangle {
protected int width;
protected int height;
public void setWidth(int width) { this.width = width; }
public void setHeight(int height) { this.height = height; }
public int area() { return width * height; }
}
public class Square extends Rectangle {
@Override
public void setWidth(int width) {
this.width = width;
this.height = width; // side effect — violates Rectangle contract!
}
@Override
public void setHeight(int height) {
this.width = height; // side effect
this.height = height;
}
}
// Client code that breaks with Square
public void testRectangle(Rectangle r) {
r.setWidth(5);
r.setHeight(4);
assert r.area() == 20 : "Expected 20, got " + r.area();
// Passes for Rectangle — fails for Square (area = 16 not 20)!
}
testRectangle(new Rectangle()); // ✅ passes
testRectangle(new Square()); // ❌ assertion fails — LSP violated
// ✅ Fix: do not use inheritance; use separate classes with a common interface
public interface Shape {
int area();
}
public final class Rectangle implements Shape {
private final int width, height;
public Rectangle(int width, int height) { this.width = width; this.height = height; }
@Override public int area() { return width * height; }
}
public final class Square implements Shape {
private final int side;
public Square(int side) { this.side = side; }
@Override public int area() { return side * side; }
}4
Interface Segregation Principle
Clients should not be forced to depend on methods they do not use — prefer many small, role-specific interfaces over one fat interface.
- ✓Fat interfaces force implementing classes to provide empty/unsupported stubs — violating ISP.
- ✓Split fat interfaces into role interfaces — each interface has one cohesive purpose.
- ✓Java's Readable, Writable, and Closeable are role interfaces (ISP in java.io).
- ✓Spring Data CrudRepository → PagingAndSortingRepository → JpaRepository is a graduated ISP hierarchy.
- ✓ISP is the interface-level application of SRP — both are about cohesion.
Java — ISP: Fat interface to role interfaces
// ❌ ISP Violation: fat interface
public interface Worker {
void work();
void eat(); // not applicable to robots
void sleep(); // not applicable to robots
void attendMeeting();
}
public class HumanWorker implements Worker {
@Override public void work() { System.out.println("Human working"); }
@Override public void eat() { System.out.println("Human eating"); }
@Override public void sleep() { System.out.println("Human sleeping"); }
@Override public void attendMeeting() { System.out.println("Human in meeting"); }
}
public class RobotWorker implements Worker {
@Override public void work() { System.out.println("Robot working"); }
@Override public void eat() { throw new UnsupportedOperationException("Robots don't eat!"); }
@Override public void sleep() { throw new UnsupportedOperationException("Robots don't sleep!"); }
@Override public void attendMeeting() { /* robots attend via video? */ }
}
// ✅ ISP Fix: role interfaces
public interface Workable { void work(); }
public interface Feedable { void eat(); }
public interface Restable { void sleep(); }
public interface MeetingCapable { void attendMeeting(); }
// Human implements all roles it needs
public class HumanWorker implements Workable, Feedable, Restable, MeetingCapable {
@Override public void work() { System.out.println("Human working"); }
@Override public void eat() { System.out.println("Human eating"); }
@Override public void sleep() { System.out.println("Human sleeping"); }
@Override public void attendMeeting() { System.out.println("Human in meeting"); }
}
// Robot only implements what it can do — no stubs, no exceptions
public class RobotWorker implements Workable, MeetingCapable {
@Override public void work() { System.out.println("Robot working"); }
@Override public void attendMeeting() { System.out.println("Robot attending via stream"); }
}
// Client depends only on the role it needs
public class WorkScheduler {
private final List<Workable> workers;
public WorkScheduler(List<Workable> workers) { this.workers = workers; }
public void startWork() { workers.forEach(Workable::work); }
// Does not care if worker is Human or Robot — depends only on Workable
}5
Dependency Inversion Principle
High-level modules should not depend on low-level modules; both should depend on abstractions — and abstractions should not depend on details.
- ✓High-level modules must not depend on low-level modules — both depend on interfaces.
- ✓The interface is "owned" by the high-level module — low-level modules implement it.
- ✓DIP makes the high-level policy stable and immune to changes in low-level details.
- ✓Dependency Injection is the runtime mechanism that wires abstractions to implementations.
- ✓Without DIP, switching from MySQL to MongoDB requires modifying every high-level class that directly imports MySQL types.
Java — DIP with Repository interface and Spring DI
// ❌ DIP violation: high-level depends on low-level concrete class
public class OrderService {
// Directly coupled to MySQL implementation
private final MySqlOrderRepository repo = new MySqlOrderRepository();
public void placeOrder(Order order) {
repo.save(order); // tied to MySQL — cannot test without DB
}
}
// ✅ DIP applied: both depend on abstraction
// 1. Define the abstraction (interface owned by the HIGH-LEVEL module)
public interface OrderRepository {
void save(Order order);
Optional<Order> findById(String id);
List<Order> findByUserId(String userId);
}
// 2. Low-level module implements the abstraction
public class MySqlOrderRepository implements OrderRepository {
private final JdbcTemplate jdbc;
public MySqlOrderRepository(JdbcTemplate jdbc) { this.jdbc = jdbc; }
@Override public void save(Order order) {
jdbc.update("INSERT INTO orders (id, user_id, total) VALUES (?, ?, ?)",
order.getId(), order.getUserId(), order.getTotal());
}
@Override public Optional<Order> findById(String id) {
return jdbc.query("SELECT * FROM orders WHERE id = ?",
ORDER_MAPPER, id).stream().findFirst();
}
@Override public List<Order> findByUserId(String userId) {
return jdbc.query("SELECT * FROM orders WHERE user_id = ?", ORDER_MAPPER, userId);
}
}
// 3. High-level module depends ONLY on the interface
@Service
public class OrderService {
private final OrderRepository orderRepo; // abstraction, not concrete
private final PaymentGateway payment;
public OrderService(OrderRepository orderRepo, PaymentGateway payment) {
this.orderRepo = orderRepo;
this.payment = payment;
}
public Order placeOrder(Cart cart) {
Order order = Order.from(cart);
payment.charge(order.getUserId(), order.getTotal());
orderRepo.save(order);
return order;
}
}
// 4. Spring wires them (DI applies DIP at runtime)
@Configuration
public class DataConfig {
@Bean
public OrderRepository orderRepository(JdbcTemplate jdbc) {
return new MySqlOrderRepository(jdbc);
// Swap to PostgreSQL: return new PostgresOrderRepository(jdbc);
// Swap to MongoDB: return new MongoOrderRepository(mongoTemplate);
}
}
// 5. In tests: inject an in-memory fake — zero database needed
class OrderServiceTest {
@Test void placeOrder_savesOrder() {
List<Order> savedOrders = new ArrayList<>();
OrderRepository fakeRepo = new OrderRepository() {
@Override public void save(Order o) { savedOrders.add(o); }
@Override public Optional<Order> findById(String id) { return Optional.empty(); }
@Override public List<Order> findByUserId(String uid) { return List.of(); }
};
OrderService sut = new OrderService(fakeRepo, new FakePaymentGateway());
sut.placeOrder(Cart.withItems(List.of(new Item("Book", 499.0))));
assertEquals(1, savedOrders.size());
}
}Learn this free with Aria, your AI tutor → AiCanCode.org/learn/lld