SOLID Principles in Java Explained with Simple Examples
A set of five principles that helps us create code that is easier to understand, test, maintain, and extend.
Let’s start with an example. Imagine a small online shop that processes orders.
1. Single Responsibility Principle — SRP
A class should have only one reason to change.
The following class has too many responsibilities:
class OrderService {
void createOrder() {
System.out.println("Order created");
}
void saveToDatabase() {
System.out.println("Order saved");
}
void sendEmail() {
System.out.println("Email sent");
}
}It creates an order, saves data, and sends an email.
A better approach is to separate these responsibilities:
class OrderService {
void createOrder() {
System.out.println("Order created");
}
}class OrderRepository {
void save() {
System.out.println("Order saved");
}
}class EmailService {
void sendConfirmation() {
System.out.println("Email sent");
}
}Now, each class has one clear job.
Easy way to remember: One class, one job.
2. Open/Closed Principle — OCP
A class should be open for extension but closed for modification.
Suppose our shop supports different payment methods.
interface PaymentMethod {
void pay(double amount);
}
class CardPayment implements PaymentMethod {
public void pay(double amount) {
System.out.println("Paid by card: " + amount);
}
}
class PayPalPayment implements PaymentMethod {
public void pay(double amount) {
System.out.println("Paid using PayPal: " + amount);
}
}The payment service works with the interface:
class PaymentService {
void processPayment(PaymentMethod method, double amount) {
method.pay(amount);
}
}If we need bank-transfer payments, we can add another class without changing PaymentService.
class BankTransferPayment implements PaymentMethod {
public void pay(double amount) {
System.out.println("Paid by bank transfer: " + amount);
}
}Easy way to remember: Add new behaviour without changing stable code.
3. Liskov Substitution Principle — LSP
A child class should be usable wherever its parent type is expected.
Every payment implementation must behave like a valid PaymentMethod.
PaymentMethod payment = new CardPayment();
payment.pay(5000);We should be able to replace it safely:
PaymentMethod payment = new PayPalPayment();
payment.pay(5000);The application should continue working correctly.
An implementation that throws UnsupportedOperationException from pay() would break this principle because it cannot properly replace the interface.
Get Dinuka Kasun Medis’s stories in your inbox
Join Medium for free to get updates from this writer.
Easy way to remember: A child must correctly replace its parent.
4. Interface Segregation Principle — ISP
A class should not be forced to implement methods it does not need.
Consider a large interface:
interface OrderOperations {
void placeOrder();
void cancelOrder();
void generateAdminReport();
}A customer may need to place and cancel orders, but should not need to implement admin reporting.
We can divide it into smaller interfaces:
interface CustomerOrderOperations {
void placeOrder();
void cancelOrder();
}interface AdminOperations {
void generateReport();
}Classes now implement only the operations they actually need.
Easy way to remember: Prefer small, focused interfaces.
5. Dependency Inversion Principle — DIP
High-level classes should depend on abstractions, not concrete classes.
A tightly coupled service looks like this:
class CheckoutService {
private final CardPayment payment = new CardPayment();
void checkout(double amount) {
payment.pay(amount);
}
}CheckoutService can only use card payments.
Instead, it should depend on the PaymentMethod interface:
class CheckoutService {
private final PaymentMethod paymentMethod;
CheckoutService(PaymentMethod paymentMethod) {
this.paymentMethod = paymentMethod;
}
void checkout(double amount) {
paymentMethod.pay(amount);
}
}Now we can inject any payment method:
public class Main {
public static void main(String[] args) {
PaymentMethod payment = new CardPayment();
CheckoutService checkout = new CheckoutService(payment);
checkout.checkout(5000);
}
}This is also the main idea behind dependency injection in Spring.
Easy way to remember: Depend on interfaces, not implementations.
Quick Summary
- S — Single Responsibility: One class should have one job.
- O — Open/Closed: Extend behaviour without modifying stable code.
- L — Liskov Substitution: A child must safely replace its parent.
- I — Interface Segregation: Keep interfaces small and focused.
- D — Dependency Inversion: Depend on abstractions instead of concrete classes.
SOLID does not mean creating an interface for every class or dividing the application into hundreds of tiny classes. The goal is to reduce coupling and make future changes safer.
The simplest way to remember SOLID is:
Give each class a clear job, make behaviour extendable, respect contracts, keep interfaces focused, and depend on abstractions.
If this article helped you understand the topic, please give it a clap 👏 and share it with someone who might find it useful.









