SOLID principles checklist with Java examples. Use when reviewing classes, refactoring code, or when user asks about Single Responsibility, Open/Closed, Liskov, Interface Segregation, or Dependency Inversion.
npx skills add https://github.com/decebals/claude-code-java --skill solid-principles
Review and apply SOLID principles in Java code.
| Letter | Principle | One-liner |
|--------|-----------|-----------|
| S | Single Responsibility | One class = one reason to change |
| O | Open/Closed | Open for extension, closed for modification |
| L | Liskov Substitution | Subtypes must be substitutable for base types |
| I | Interface Segregation | Many specific interfaces > one general interface |
| D | Dependency Inversion | Depend on abstractions, not concretions |
> "A class should have only one reason to change."
// ❌ BAD: UserService does too much
public class UserService {
public User createUser(String name, String email) {
// validation logic
if (email == null || !email.contains("@")) {
throw new IllegalArgumentException("Invalid email");
}
// persistence logic
User user = new User(name, email);
entityManager.persist(user);
// notification logic
String subject = "Welcome!";
String body = "Hello " + name;
emailClient.send(email, subject, body);
// audit logic
auditLog.log("User created: " + email);
return user;
}
}
Problems:
// ✅ GOOD: Each class has one responsibility
public class UserValidator {
public void validate(String name, String email) {
if (email == null || !email.contains("@")) {
throw new ValidationException("Invalid email");
}
}
}
public class UserRepository {
public User save(User user) {
entityManager.persist(user);
return user;
}
}
public class WelcomeEmailSender {
public void sendWelcome(User user) {
String subject = "Welcome!";
String body = "Hello " + user.getName();
emailClient.send(user.getEmail(), subject, body);
}
}
public class UserAuditLogger {
public void logCreation(User user) {
auditLog.log("User created: " + user.getEmail());
}
}
public class UserService {
private final UserValidator validator;
private final UserRepository repository;
private final WelcomeEmailSender emailSender;
private final UserAuditLogger auditLogger;
public User createUser(String name, String email) {
validator.validate(name, email);
User user = repository.save(new User(name, email));
emailSender.sendWelcome(user);
auditLogger.logCreation(user);
return user;
}
}
import statements from different domains> "Software entities should be open for extension, but closed for modification."
// ❌ BAD: Must modify class to add new discount type
public class DiscountCalculator {
public double calculate(Order order, String discountType) {
if (discountType.equals("PERCENTAGE")) {
return order.getTotal() * 0.1;
} else if (discountType.equals("FIXED")) {
return 50.0;
} else if (discountType.equals("LOYALTY")) {
return order.getTotal() * order.getCustomer().getLoyaltyRate();
}
// Every new discount type = modify this class
return 0;
}
}
// ✅ GOOD: Add new discounts without modifying existing code
public interface DiscountStrategy {
double calculate(Order order);
boolean supports(String discountType);
}
public class PercentageDiscount implements DiscountStrategy {
@Override
public double calculate(Order order) {
return order.getTotal() * 0.1;
}
@Override
public boolean supports(String discountType) {
return "PERCENTAGE".equals(discountType);
}
}
public class FixedDiscount implements DiscountStrategy {
@Override
public double calculate(Order order) {
return 50.0;
}
@Override
public boolean supports(String discountType) {
return "FIXED".equals(discountType);
}
}
public class LoyaltyDiscount implements DiscountStrategy {
@Override
public double calculate(Order order) {
return order.getTotal() * order.getCustomer().getLoyaltyRate();
}
@Override
public boolean supports(String discountType) {
return "LOYALTY".equals(discountType);
}
}
// New discount? Just add new class, no modification needed
public class SeasonalDiscount implements DiscountStrategy {
@Override
public double calculate(Order order) {
return order.getTotal() * 0.2;
}
@Override
public boolean supports(String discountType) {
return "SEASONAL".equals(discountType);
}
}
public class DiscountCalculator {
private final List<DiscountStrategy> strategies;
public DiscountCalculator(List<DiscountStrategy> strategies) {
this.strategies = strategies;
}
public double calculate(Order order, String discountType) {
return strategies.stream()
.filter(s -> s.supports(discountType))
.findFirst()
.map(s -> s.calculate(order))
.orElse(0.0);
}
}
if/else or switch on type/status that grows over time| Pattern | Use When |
|---------|----------|
| Strategy | Multiple algorithms for same operation |
| Template Method | Same structure, different steps |
| Decorator | Add behavior dynamically |
| Factory | Create objects without specifying class |
> "Subtypes must be substitutable for their base types."
// ❌ BAD: Square violates Rectangle contract
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 getArea() {
return width * height;
}
}
public class Square extends Rectangle {
@Override
public void setWidth(int width) {
this.width = width;
this.height = width; // Violates expected behavior!
}
@Override
public void setHeight(int height) {
this.width = height; // Violates expected behavior!
this.height = height;
}
}
// This test fails for Square!
void testRectangle(Rectangle r) {
r.setWidth(5);
r.setHeight(4);
assert r.getArea() == 20; // Square returns 16!
}
// ✅ GOOD: Separate abstractions
public interface Shape {
int getArea();
}
public class Rectangle implements Shape {
private final int width;
private final int height;
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
@Override
public int getArea() {
return width * height;
}
}
public class Square implements Shape {
private final int side;
public Square(int side) {
this.side = side;
}
@Override
public int getArea() {
return side * side;
}
}
| Rule | Meaning |
|------|---------|
| Preconditions | Subclass cannot strengthen (require more) |
| Postconditions | Subclass cannot weaken (promise less) |
| Invariants | Subclass must maintain parent's invariants |
| History | Subclass cannot modify inherited state unexpectedly |
instanceof checks before calling methods// If you see this, LSP might be violated
if (bird instanceof Penguin) {
// don't call fly()
} else {
bird.fly();
}
> "Clients should not be forced to depend on interfaces they do not use."
// ❌ BAD: Fat interface forces unnecessary implementations
public interface Worker {
void work();
void eat();
void sleep();
void attendMeeting();
void writeReport();
}
// Robot can't eat or sleep!
public class Robot implements Worker {
@Override public void work() { /* OK */ }
@Override public void eat() { /* Can't eat! */ }
@Override public void sleep() { /* Can't sleep! */ }
@Override public void attendMeeting() { /* OK */ }
@Override public void writeReport() { /* Maybe */ }
}
// Intern doesn't attend meetings or write reports
public class Intern implements Worker {
@Override public void work() { /* OK */ }
@Override public void eat() { /* OK */ }
@Override public void sleep() { /* OK */ }
@Override public void attendMeeting() { /* Not allowed! */ }
@Override public void writeReport() { /* Not expected! */ }
}
// ✅ GOOD: Segregated interfaces
public interface Workable {
void work();
}
public interface Feedable {
void eat();
void sleep();
}
public interface Manageable {
void attendMeeting();
void writeReport();
}
// Combine what you need
public class Employee implements Workable, Feedable, Manageable {
@Override public void work() { /* ... */ }
@Override public void eat() { /* ... */ }
@Override public void sleep() { /* ... */ }
@Override public void attendMeeting() { /* ... */ }
@Override public void writeReport() { /* ... */ }
}
public class Robot implements Workable {
@Override public void work() { /* ... */ }
// No unnecessary methods!
}
public class Intern implements Workable, Feedable {
@Override public void work() { /* ... */ }
@Override public void eat() { /* ... */ }
@Override public void sleep() { /* ... */ }
// No meeting/report methods!
}
throw new UnsupportedOperationException()// java.util.List has many methods - but this is acceptable for collections
// However, be careful with your own interfaces!
// ❌ This interface is too fat for most use cases
public interface Repository<T> {
T findById(Long id);
List<T> findAll();
T save(T entity);
void delete(T entity);
void deleteById(Long id);
List<T> findByExample(T example);
Page<T> findAll(Pageable pageable);
List<T> findAllById(Iterable<Long> ids);
long count();
boolean existsById(Long id);
// ... 20 more methods
}
// ✅ Better: Split by use case
public interface ReadRepository<T> {
Optional<T> findById(Long id);
List<T> findAll();
}
public interface WriteRepository<T> {
T save(T entity);
void delete(T entity);
}
> "High-level modules should not depend on low-level modules. Both should depend on abstractions."
// ❌ BAD: High-level depends on low-level directly
public class OrderService {
private MySqlOrderRepository repository; // Concrete class!
private SmtpEmailSender emailSender; // Concrete class!
public OrderService() {
this.repository = new MySqlOrderRepository(); // Hard dependency
this.emailSender = new SmtpEmailSender(); // Hard dependency
}
public void createOrder(Order order) {
repository.save(order);
emailSender.send(order.getCustomerEmail(), "Order confirmed");
}
}
Problems:
// ✅ GOOD: Depend on abstractions
// Abstractions (interfaces)
public interface OrderRepository {
void save(Order order);
Optional<Order> findById(Long id);
}
public interface NotificationSender {
void send(String recipient, String message);
}
// High-level module depends on abstractions
public class OrderService {
private final OrderRepository repository;
private final NotificationSender notificationSender;
// Dependencies injected
public OrderService(OrderRepository repository,
NotificationSender notificationSender) {
this.repository = repository;
this.notificationSender = notificationSender;
}
public void createOrder(Order order) {
repository.save(order);
notificationSender.send(order.getCustomerEmail(), "Order confirmed");
}
}
// Low-level modules implement abstractions
public class MySqlOrderRepository implements OrderRepository {
@Override
public void save(Order order) { /* MySQL specific */ }
@Override
public Optional<Order> findById(Long id) { /* MySQL specific */ }
}
public class SmtpEmailSender implements NotificationSender {
@Override
public void send(String recipient, String message) { /* SMTP specific */ }
}
// Easy to test with mocks!
public class InMemoryOrderRepository implements OrderRepository {
private Map<Long, Order> orders = new HashMap<>();
@Override
public void save(Order order) {
orders.put(order.getId(), order);
}
@Override
public Optional<Order> findById(Long id) {
return Optional.ofNullable(orders.get(id));
}
}
// Spring handles dependency injection automatically
@Service
public class OrderService {
private final OrderRepository repository;
private final NotificationSender notificationSender;
// Constructor injection (recommended)
public OrderService(OrderRepository repository,
NotificationSender notificationSender) {
this.repository = repository;
this.notificationSender = notificationSender;
}
}
@Repository
public class JpaOrderRepository implements OrderRepository {
// Spring provides implementation
}
@Component
@Profile("production")
public class SmtpEmailSender implements NotificationSender { }
@Component
@Profile("test")
public class MockEmailSender implements NotificationSender { }
new ConcreteClass() inside business logiccom.mysql, org.apache.http)When reviewing code, check:
| Principle | Question |
|-----------|----------|
| SRP | Does this class have more than one reason to change? |
| OCP | Will adding a new type/feature require modifying this class? |
| LSP | Can subclasses be used wherever parent is expected? |
| ISP | Are there empty or throwing method implementations? |
| DIP | Does high-level code depend on concrete implementations? |
| Violation | Refactoring |
|-----------|-------------|
| SRP - God class | Extract Class, Move Method |
| OCP - Type switching | Strategy Pattern, Factory |
| LSP - Broken inheritance | Composition over Inheritance, Extract Interface |
| ISP - Fat interface | Split Interface, Role Interface |
| DIP - Hard dependencies | Dependency Injection, Abstract Factory |
design-patterns - Implementation patterns (Factory, Strategy, Observer, etc.)clean-code - Code-level principles (DRY, KISS, naming)java-code-review - Comprehensive review checklistExecute git commit with conventional commit message analysis, intelligent staging, and message generation. Use when user asks to commit changes, create a git commit, or mentions "/commit". Supports: (1) Auto-detecting type and scope from changes, (2) Generating conventional commit messages from diff, (3) Interactive commit with optional type/scope/description overrides, (4) Intelligent file staging for logical grouping
Comprehensive GitHub code review with AI-powered swarm coordination
Create high-quality git commits: review/stage intended changes, split into logical commits, and write clear commit messages (including Conventional Commits). Use when the user asks to commit, craft a commit message, stage changes, or split work into multiple commits.
Comprehensive truth scoring, code quality verification, and automatic rollback system with 0.95 accuracy threshold for ensuring high-quality agent outputs and codebase reliability.
GitHub CLI (gh) comprehensive reference for repositories, issues, pull requests, Actions, projects, releases, gists, codespaces, organizations, extensions, and all GitHub operations from the command line.
GitHub CLI - manage repositories, issues, pull requests, actions, releases, and more from the command line.
You are a code refactoring expert specializing in clean code principles, SOLID design patterns, and modern software engineering best practices. Analyze and refactor the provided code to improve its quality, maintainability, and performance.
You are a technical debt expert specializing in identifying, quantifying, and prioritizing technical debt in software projects. Analyze the codebase to uncover debt, assess its impact, and create acti
Take decebals/solid-principles from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.