Mahamudul Hasan Rubel
HomeBlogCoursesAboutProjectsSkillsExperiencePhotosContact
Mahamudul Hasan Rubel

Senior Software Engineer crafting high-performance web applications and SaaS platforms.

Navigation

  • Home
  • Blog
  • Courses
  • About
  • Projects
  • Skills
  • Experience
  • Photos
  • Contact

Get in Touch

Available for senior/lead roles and consulting.

bd.mhrubel@gmail.comHire Me

Subscribe to the newsletter

Get new articles and course lessons delivered to your inbox. No spam, unsubscribe anytime.

© 2026 Mahamudul Hasan Rubel. All rights reserved.

Built with using Next.js 16 & Tailwind v4

Back to Blog
Lesson 18 of the Advanced Laravel: Architecture, Scaling & Performance course
LaravelJune 27, 20264 min read

Event-Driven Architecture: Decoupling Laravel Domain Modules

Master event-driven architecture in Laravel to decouple your domain modules. Learn to dispatch events and register listeners for scalable, maintainable code.

LaravelArchitectureEventsDecouplingDomain-Driven Designphpbackend

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.

The Problem: Tight Coupling Across Domains

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.

Decoupling with Events

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

Dispatching Events Across Boundaries

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:

PHP
namespace 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.

Registering Listeners in Providers

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:

PHP
namespace 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.

Advanced Usage: Queued Listeners

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.

PHP
namespace 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
    }
}

Hands-on Exercise: Decoupling User Registration

  1. Identify a cross-module dependency: Look at your current user registration logic. Is it directly triggering a "Welcome Notification" or "Add to CRM" service?
  2. Create an Event: Define a UserRegistered event inside your User module.
  3. Dispatch: Update your RegisterUserAction to dispatch this event.
  4. Register a Listener: Create a new listener in your Notification module that catches UserRegistered and sends the welcome email.
  5. Verify: Ensure that the User module has no use statements referencing the Notification namespace.

Common Pitfalls

  • Passing Models vs. IDs: Avoid passing entire Eloquent models in events if you are queueing them. If the model changes between the event dispatch and the job execution, you might have stale data. Pass the ID and refetch the model in the listener.
  • Event Loops: Be careful not to dispatch an event in a listener that triggers the same event, causing an infinite loop.
  • Over-Engineering: Don't turn every method call into an event. Use events for cross-boundary communication. If two classes live in the same module and are tightly related, a direct service call is often more readable and easier to debug.

Recap

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.

Previous lessonRate Limiting Background JobsNext lesson Integrating External Message Brokers
Back to Blog

Similar Posts

LaravelJune 26, 20263 min read

Introduction to Laravel Events and Listeners for Clean Code

Learn how to use Laravel events and listeners to decouple secondary side effects from your primary business logic, resulting in cleaner, maintainable code.

Read more
LaravelJune 28, 20263 min read

Graceful Degradation: Implementing Circuit Breakers in Laravel

Graceful degradation ensures your Laravel application stays functional when dependencies fail. Learn to implement circuit breakers and robust fallbacks.

Part of the course

Advanced Laravel: Architecture, Scaling & Performance

advanced · Lesson 18 of 57

  1. 1

    Transitioning from MVC to DDD

    3 min
  2. 2

    Defining Bounded Contexts

    3 min
  3. 3

    Implementing Action Classes

    3 min
Read more
LaravelJune 28, 20264 min read

Database Indexing for Joins: Architecting High-Performance Queries

Master SQL indexing for joins by learning to analyze execution plans and build covering indexes that eliminate table scans in high-traffic Laravel applications.

Read more
4

Utilizing Data Transfer Objects (DTOs)

3 min
  • 5

    Service Layer Pattern

    4 min
  • 6

    Modular Monolith Structure

    3 min
  • 7

    Querying with Strict Eloquent

    4 min
  • 8

    Advanced Subqueries and Joins

    4 min
  • 9

    Raw Expressions for Performance

    4 min
  • 10

    Advanced Indexing Strategies

    4 min
  • 11

    Database Partitioning Techniques

    4 min
  • 12

    Read/Write Database Splitting

    4 min
  • 13

    Handling Multi-Database Connections

    3 min
  • 14

    Eloquent Caching Strategies

    3 min
  • 15

    Queue Worker Prioritization

    4 min
  • 16

    Unique Job Patterns

    4 min
  • 17

    Rate Limiting Background Jobs

    3 min
  • 18

    Event-Driven Architecture

    4 min
  • 19

    Integrating External Message Brokers

    4 min
  • 20

    Distributed Transactions and Sagas

    3 min
  • 21

    Eventual Consistency Patterns

    4 min
  • 22

    Multi-Layered Caching Strategy

    4 min
  • 23

    Cache Tagging and Invalidation

    4 min
  • 24

    Session Persistence in Clusters

    4 min
  • 25

    High-Availability Infrastructure

    4 min
  • 26

    Zero-Downtime Deployment Pipelines

    4 min
  • 27

    Advanced OAuth2 Implementation

    3 min
  • 28

    JWT and Stateless Security

    4 min
  • 29

    Multi-Tenant Security Isolation

    3 min
  • 30

    Defense Against SSRF

    3 min
  • 31

    Mass Assignment Hardening

    4 min
  • 32

    Automated Security Testing

    3 min
  • 33

    Custom Telemetry Design

    3 min
  • 34

    Distributed Tracing

    4 min
  • 35

    Profiling PHP Execution

    3 min
  • 36

    Memory Management in Long-Running Processes

    4 min
  • 37

    Testing DDD Components

    3 min
  • 38

    Contract Testing

    3 min
  • 39

    Handling Large File Uploads

    3 min
  • 40

    Optimizing Asset Pipelines

    4 min
  • 41

    Database Query Caching Layers

    3 min
  • 42

    Advanced Eloquent Scopes

    4 min
  • 43

    Distributed Locks

    3 min
  • 44

    API Versioning Strategies

    4 min
  • 45

    Database Migration Strategies

    4 min
  • 46

    Handling Webhooks Securely

    3 min
  • 47

    Advanced Logging Patterns

    3 min
  • 48

    Database Indexing for Joins

    4 min
  • 49

    Graceful Degradation

    3 min
  • 50

    Custom Middleware Development

    Coming soon
  • 51

    Database Connection Pooling

    Coming soon
  • 52

    Handling Large Data Exports

    Coming soon
  • 53

    Security Header Configuration

    Coming soon
  • 54

    Database Sharding Concepts

    Coming soon
  • 55

    Real-time Data Synchronization

    Coming soon
  • 56

    Database Deadlock Prevention

    Coming soon
  • 57

    Managing Third-Party API Integrations

    Coming soon
  • View full course