Clean repositories and services in Laravel 11 — pragmatic SOLID
Everything here comes from shipping dozens of real-customer APIs. The goal is not "SOLID for its own sake"; it is two things: testable code and clear layers that never mix.The layer cake we default toRoute: endpoint name…

Everything here comes from shipping dozens of real-customer APIs. The goal is not "SOLID for its own sake"; it is two things: testable code and clear layers that never mix.
The layer cake we default to
- Route: endpoint name + middleware.
- FormRequest: validation only — no translation strings, no business logic.
- Controller: try/catch, hands input to the Service, wraps the result in a Resource via a trait.
- Service: business logic lives here. It is the only place that touches the Model.
- Resource: final JSON shape — nothing more.
Golden rule: the controller knows nothing about Eloquent; the service knows nothing about HTTP.
A consistent ResponsesTrait across every API
Every response is wrapped in the same envelope: { success, msg, data, meta? }. The frontend (Next.js) depends on this exact contract — any change is a breaking change.
When do we introduce a Repository?
We do not invent repositories for fun. We add one when:
- the same query shape appears in 3+ places;
- we need an interface to fake in tests;
- we want a unified cache layer without polluting the service.
Events & domain events
Services fire events rather than calling each other. Example: ProjectPublished triggers an observer that flushes the cache and fires the revalidate webhook to Next.js.
Testing in practice
We aim to test every service without touching the database: swap the repository with a fake in the container and call the service directly. Pest makes this a joy.
Anti-patterns we avoid
- Putting business logic inside Eloquent model events.
- Sprinkling facades throughout services — DI is clearer.
- Letting controllers touch models directly.


