Master event-driven architecture in Laravel to decouple your domain modules. Learn to dispatch events and register listeners for scalable, maintainable code.
Previously in this course, we discussed the Modular Monolith Structure, where we organized our application into distinct domain-specific directories. While this structure physicalized our boundaries, it didn't solve the problem of cross-module dependencies.
In this lesson, we move from direct service-to-service coupling toward an event-driven architecture. By using Laravel’s internal event bus, we can allow our modules to communicate without needing to know about each other's internal implementation details.
When you build a SaaS platform, domains naturally overlap. For example, when a User completes a purchase in your Billing module, the Marketing module might need to send a welcome email, and the Analytics module might need to log the transaction.
If your BillingService directly calls MarketingService and AnalyticsService, you've created a "distributed big ball of mud." If you decide to swap your email provider or change how analytics are tracked, you have to modify the Billing module—a clear violation of the Single Responsibility Principle.
Instead of direct calls, the Billing module should simply broadcast a fact: "An order was completed." Any other module that cares about that fact can listen for it independently.
Flow diagram: Billing Module → Dispatches Event Bus; Event Bus → Marketing Module; Event Bus → Analytics Module
In a domain-driven system, we define events as "Domain Events." These represent something that happened in the past. To keep our code clean, we define these events within the domain that owns the logic.
Let's assume we are in our Billing module. We define a OrderCompleted event:
PHPnamespace Modules\Billing\Events; use Modules\Billing\Models\Order; readonly class OrderCompleted { public function __construct( public Order $order ) {} }
Now, in our CheckoutAction (referencing our work on Implementing Action Classes), we dispatch this event after the database transaction succeeds:
PHP#6A9955">// Inside Modules\Billing\Actions\CheckoutAction.php public function execute(array $data): Order { return DB::transaction(function () use ($data) { $order = Order::create($data); #6A9955">// Dispatch the event event(new \Modules\Billing\Events\OrderCompleted($order)); return $order; }); }
Notice that CheckoutAction knows nothing about who is listening. It only knows that an order was completed.
To wire these components together, we use the EventServiceProvider within our specific module. This keeps the configuration localized.
In your Modules/Marketing/Providers/EventServiceProvider.php:
PHPnamespace Modules\Marketing\Providers; use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; use Modules\Billing\Events\OrderCompleted; use Modules\Marketing\Listeners\SendWelcomeEmail; class EventServiceProvider extends ServiceProvider { protected $listen = [ OrderCompleted::class => [ SendWelcomeEmail::class, ], ]; }
By registering the listener here, we maintain a clear separation. The Marketing module "subscribes" to the Billing module's event. If we decide to remove the Marketing module entirely, we simply delete its provider, and the Billing module continues to function perfectly.
In high-traffic systems, you should never perform blocking operations (like sending emails or hitting third-party APIs) inside the main request cycle. Laravel makes it trivial to offload these to the queue by implementing the ShouldQueue interface on your listener.
PHPnamespace Modules\Marketing\Listeners; use Illuminate\Contracts\Queue\ShouldQueue; use Modules\Billing\Events\OrderCompleted; class SendWelcomeEmail implements ShouldQueue { public function handle(OrderCompleted $event): void { #6A9955">// This will now run in the background } }
UserRegistered event inside your User module.RegisterUserAction to dispatch this event.Notification module that catches UserRegistered and sends the welcome email.User module has no use statements referencing the Notification namespace.Event-driven architecture is the glue that holds a modular monolith together. By dispatching events from your domain actions and registering listeners in service providers, you effectively decouple your services. This allows your SaaS platform to evolve, as modules can be added, removed, or refactored without triggering a cascade of breaking changes.
Up next: Integrating External Message Brokers — moving beyond the internal bus to distributed systems.
Learn how to use Laravel events and listeners to decouple secondary side effects from your primary business logic, resulting in cleaner, maintainable code.
Read moreGraceful degradation ensures your Laravel application stays functional when dependencies fail. Learn to implement circuit breakers and robust fallbacks.
Event-Driven Architecture
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