Master the Service Layer pattern in Laravel to decouple domain logic from your controllers. Improve testability, maintainability, and clean architecture.
Previously in this course, we explored implementing action classes to handle single-purpose domain operations. While Actions are excellent for specific tasks, a Service Layer provides the necessary orchestration for complex business processes that span multiple domain entities or external integrations.
By moving business logic out of controllers and into dedicated service classes, you achieve true decoupling of your application's core functionality from its delivery mechanism (HTTP requests, CLI commands, or queued jobs).
In a standard Laravel application, it's tempting to put logic inside controllers. As your SaaS platform grows, these controllers become "fat," leading to code duplication, difficult unit testing, and rigid dependencies.
A Service Layer acts as a mediator. It doesn't replace models or actions; it coordinates them to fulfill a business requirement. When you use Dependency Injection to resolve these services, you make your code modular and significantly easier to mock during testing.
Let's advance our SaaS project by creating a SubscriptionService. This service will handle the complex logic of upgrading a user's plan, which involves interacting with the database, our payment gateway, and broadcasting notifications.
Always start with an interface to ensure your code remains swappable.
PHPnamespace App\Services\Contracts; use App\Models\User; use App\Models\Plan; interface SubscriptionServiceInterface { public function upgrade(User $user, Plan $plan): bool; }
The service orchestrates existing actions and repositories. Note how we inject dependencies via the constructor.
PHPnamespace App\Services; use App\Services\Contracts\SubscriptionServiceInterface; use App\Models\User; use App\Models\Plan; use App\Actions\Billing\ProcessPaymentAction; use Illuminate\Support\Facades\DB; class SubscriptionService implements SubscriptionServiceInterface { public function __construct( protected ProcessPaymentAction $paymentAction ) {} public function upgrade(User $user, Plan $plan): bool { return DB::transaction(function () use ($user, $plan) { #6A9955">// Orchestrate business logic $this->paymentAction->execute($user, $plan->price); $user->update(['plan_id' => $plan->id]); return true; }); } }
To leverage Laravel's container, bind the interface to the implementation in your AppServiceProvider or a dedicated DomainServiceProvider.
PHPpublic function register() { $this->app->bind( \App\Services\Contracts\SubscriptionServiceInterface::class, \App\Services\SubscriptionService::class ); }
Because we've registered our service in the container, injecting it is trivial. Laravel automatically resolves the dependency.
In a Controller:
PHPpublic function store(Request $request, SubscriptionServiceInterface $service) { $service->upgrade($request->user(), $request->plan); return response()->json(['message' => 'Upgraded successfully']); }
In a Queued Job:
PHPclass ProcessUserMigration implements ShouldQueue { public function handle(SubscriptionServiceInterface $service) { #6A9955">// The container resolves the service even in background jobs $service->upgrade($this->user, $this->newPlan); } }
| Feature | Action Class | Service Layer |
|---|---|---|
| Scope | Single responsibility (e.g., CreateUser) | Orchestration (e.g., UserLifecycleService) |
| Complexity | Low | High |
| Primary Goal | Encapsulate logic | Provide a domain API |
| Usage | Called by Services or Controllers | Called by Controllers or Jobs |
UserOnboardingService) to encapsulate this orchestration.AppService that handles everything. Keep services aligned with bounded contexts.ServiceA depends on ServiceB, ensure you aren't creating circular dependencies. If you find yourself doing this, reconsider your domain boundaries.By implementing a Service Layer, you decouple your business rules from the HTTP layer. This makes your application easier to test, more modular, and resilient to change. Remember to use interfaces to maintain flexibility and leverage the service container for clean dependency management.
Up next: We will explore how to organize these services into a Modular Monolith Structure to keep your domain boundaries strictly enforced as the codebase scales.
Master Testing DDD components in Laravel. Learn to mock external services, isolate domain logic, and write reliable PHPUnit tests for your Action classes.
Read moreLearn to build a Modular Monolith by structuring your Laravel directory by domain, enforcing encapsulation, and defining public interfaces for module communication.
Service Layer Pattern
Advanced Logging Patterns
Database Indexing for Joins
Graceful Degradation
Custom Middleware Development
Database Connection Pooling
Handling Large Data Exports
Security Header Configuration
Database Sharding Concepts
Real-time Data Synchronization
Database Deadlock Prevention
Managing Third-Party API Integrations