SOLID in Laravel: What It Actually Looks Like in Real Code
Most Laravel apps start clean.
Six months later, controllers are 500 lines long, models know too much
about the world, and "just one more quick change" becomes a risky
operation.
Laravel gives you powerful tools.
SOLID gives you discipline.
Let's look at what SOLID actually means in a Laravel application --- not
in theory, but in real production structure.
The SOLID Principles (Quick Refresher)
-
S -- Single Responsibility Principle
A class should have one reason to change. -
O -- Open/Closed Principle
Software entities should be open for extension but closed for
modification. -
L -- Liskov Substitution Principle
Subtypes must be substitutable for their base types. -
I -- Interface Segregation Principle
Clients should not be forced to depend on methods they do not use. -
D -- Dependency Inversion Principle
Depend on abstractions, not concrete implementations.
Now let's map that to Laravel.
S --- Single Responsibility in Laravel
❌ Fat Controller
public function store(Request $request)
{
$validated = $request->validate([
'email' => 'required|email',
'name' => 'required|string'
]);
$user = User::create($validated);
Mail::to($user->email)->send(new WelcomeMail($user));
return response()->json($user);
}
This controller: - Validates - Creates data - Sends mail - Returns
formatted output
Too many responsibilities.
✅ Cleaner Structure
Form Request Handles validation & authorization.
Service Handles business logic.
Resource Formats output.
public function store(StoreUserRequest $request, UserService $service)
{
$user = $service->createUser($request->validated());
return new UserResource($user);
}
Now:
- Controllers coordinate.
- Requests validate.
- Services execute business rules.
- Resources transform output.
- Models represent data --- not workflows.
That's SRP in Laravel.
O --- Open/Closed Principle
Let's say you support multiple payment processors.
interface PaymentProcessor
{
public function charge(Order $order): void;
}
class StripeProcessor implements PaymentProcessor
{
public function charge(Order $order): void
{
// Stripe logic
}
}
class PayPalProcessor implements PaymentProcessor
{
public function charge(Order $order): void
{
// PayPal logic
}
}
Bind it in a service provider:
$this->app->bind(PaymentProcessor::class, StripeProcessor::class);
Now you can extend behavior without modifying the calling code.
That's Open/Closed done right.
L --- Liskov Substitution Principle
If something implements a contract, it should behave as expected.
If StripeProcessor implements PaymentProcessor, it must fully honor
that contract.
Don't override behavior in child classes in a way that changes meaning.
If your code depends on PaymentProcessor, it should not care which
processor it gets.
I --- Interface Segregation
Avoid "God interfaces."
Bad:
interface UserManager
{
public function create();
public function update();
public function delete();
public function exportCsv();
public function sendNewsletter();
}
Better: split responsibilities.
UserWriterUserExporterUserNotifier
Keep contracts small and focused.
Laravel's contracts follow this pattern well.
D --- Dependency Inversion
Instead of this:
public function __construct(StripeProcessor $processor)
Do this:
public function __construct(PaymentProcessor $processor)
Then bind the implementation in a provider.
Now your business logic depends on abstractions.
Testing becomes easier.
Swapping implementations becomes trivial.
This is where Laravel's container shines.
Laravel Components & Their Responsibilities
Component Responsibility
Controller Coordinate request and response
Form Request Validation and authorization
Model Represent data and relationships
Service Business rules and orchestration
Resource Output transformation
Job Background execution
Event Signal that something happened
Listener React to an event
Policy Authorization logic
Facade Convenience access, not core domain logic
Respecting these boundaries keeps your app maintainable.
A Warning: SOLID Does Not Mean Class Explosion
SOLID is about clarity, not ceremony.
If you create:
- UserCreator\
- UserValidator\
- UserLogger\
- UserEventDispatcher
...for a feature that does not need it, you are adding noise.
Use structure where complexity demands it.
Discipline should reduce friction, not create it.
Final Thoughts
Laravel gives you the tools.
SOLID gives you the guardrails.
Together, they produce applications that survive version two --- and
version five.
Structure is not about being clever.
It's about making future changes boring and safe.
That's real architecture.
Member discussion