Back to Blog
SOLID Principles in Laravel: Practical Examples From Real Projects
Learn how to apply SOLID principles in Laravel using practical examples such as payment gateways, service classes, repositories, and Laravel's service container.
Back
Backend

Table of contents
Laravel developers often hear about SOLID principles.
Unfortunately, most tutorials explain SOLID using examples like Bird, Penguin, Rectangle, or Square.
Those examples are useful for understanding the theory. But in real Laravel projects, we usually deal with things like:
- payment gateways
- shipping providers
- service classes
- repositories
- notifications
- queues
- third-party integrations
That makes SOLID feel much more practical than academic.
The good news is that Laravel already encourages many SOLID practices by default through features like dependency injection, the service container, contracts, events, jobs, and middleware.
Many Laravel developers are already using SOLID without even realizing it.
Let's look at what SOLID actually means in the context of real Laravel projects.
Single Responsibility Principle (SRP)
A class should have only one reason to change.
This is probably the most violated principle in Laravel applications.
Many projects eventually end up with controllers like this:
php
class OrderController extends Controller
{
public function store(Request $request)
{
// validation
// create order
// update inventory
// create invoice
// send email
// create shipment
// send notification
}
}The problem is simple: the controller now has multiple responsibilities.
If inventory changes, the controller changes. If invoice generation changes, the controller changes. If email templates change, the controller changes. If shipment logic changes, the controller changes.
This usually leads to what Laravel developers call a "fat controller".
A better approach might look like this:
php
class OrderController extends Controller
{
public function store(
StoreOrderRequest $request,
CreateOrderService $service
) {
$service->execute($request->validated());
}
}Then move responsibilities into dedicated classes:
CreateOrderServiceUpdateInventoryActionGenerateInvoiceActionCreateShipmentActionSendOrderConfirmationJob
Each class now has a single reason to change.
Open Closed Principle (OCP)
Software entities should be open for extension but closed for modification.
A common Laravel example is payment gateways.
Many applications start like this:
php
if ($provider === 'stripe') {
//
} elseif ($provider === 'midtrans') {
//
} elseif ($provider === 'xendit') {
//
}This works—until a new payment provider arrives. Now you modify the existing logic again.
A better solution is using an interface:
php
interface PaymentGateway
{
public function charge(array $payload): PaymentResult;
}Implementations become:
text
StripePaymentGateway
MidtransPaymentGateway
XenditPaymentGatewayAdding a new provider becomes:
text
ToyyibPayPaymentGatewayNo existing payment code needs to change. You extend the system without modifying existing behavior.
That is Open Closed Principle.
Liskov Substitution Principle (LSP)
Subclasses should be replaceable by their parent abstraction.
This is usually the hardest SOLID principle to explain.
Imagine this interface:
php
interface PaymentGateway
{
public function charge(array $payload);
public function refund(string $transactionId);
}If your application expects every payment gateway to support refunds, every implementation should behave consistently.
This should work:
php
function refundOrder(PaymentGateway $gateway, string $transactionId): void
{
$gateway->refund($transactionId);
}If one implementation suddenly throws Refund not supported, then the abstraction may be incorrect.
The issue is not the implementation. The issue is that the interface promised something that not every implementation could actually do.
Interface Segregation Principle (ISP)
Clients should not be forced to depend on methods they do not use.
Continuing the payment example:
php
interface PaymentGateway
{
public function charge();
public function refund();
public function payout();
public function subscription();
public function splitPayment();
}The problem? Not every provider supports every feature.
Some providers only support charging payments. Others support subscriptions. Some support payouts.
Instead of one large interface, split them:
php
interface Chargeable
{
public function charge();
}
interface Refundable
{
public function refund();
}
interface SubscriptionCapable
{
public function subscribe();
}Smaller interfaces usually lead to cleaner designs.
Dependency Inversion Principle (DIP)
Depend on abstractions, not concrete implementations.
This is probably the SOLID principle Laravel developers use the most.
Bad example:
php
class OrderService
{
protected StripePaymentGateway $gateway;
public function __construct()
{
$this->gateway = new StripePaymentGateway();
}
}The service is now tightly coupled to Stripe. Changing to Midtrans means changing the service itself.
A better approach:
php
class OrderService
{
public function __construct(
protected PaymentGateway $gateway
) {}
}Then register the implementation inside a service provider:
php
$this->app->bind(
PaymentGateway::class,
StripePaymentGateway::class
);Now your service depends on an abstraction. Changing providers becomes easy. Testing becomes easier. Mocking becomes easier. Maintenance becomes easier.
This is exactly what Laravel's service container was designed for.
Does Every Laravel Project Need SOLID?
No.
Small CRUD applications do not need six abstraction layers. Creating interfaces for everything often creates more complexity than value.
SOLID is not about writing more classes. SOLID is about managing change.
If the code is unlikely to change, simple code is often better. If the code will grow, integrate with multiple providers, or be maintained by multiple developers, SOLID starts becoming very valuable.
My Take
Laravel already makes SOLID easier than plain PHP.
Dependency Injection, Service Container, Contracts, Jobs, Events, Notifications, and Middleware all naturally encourage better separation of concerns.
The biggest mistake is not ignoring SOLID. The biggest mistake is forcing SOLID everywhere.
Good engineering is usually about balance. Sometimes an interface is the right decision. Sometimes a simple class is enough.
The goal is not to follow principles perfectly. The goal is to make future changes cheaper and safer.
That is what SOLID was trying to solve in the first place.
Related reading: Refactoring the Beast: A Pragmatic Guide to Legacy Code, Scaling Laravel Applications, Best PHP Framework 2026.
Comments
No comments yet
Loading comments...