DEV Community

Cover image for Modular monolith architecture within Laravel, communication between different modules.
Md Abu Musa
Md Abu Musa

Posted on

2

Modular monolith architecture within Laravel, communication between different modules.

In a modular monolith architecture within Laravel, communication between different modules can be achieved in several ways, depending on the level of coupling and design preferences. Here are a few common approaches:

1. Direct Method Calls

  • This is the simplest approach where one module directly calls public methods or services from another module. You can use Laravel’s Dependency Injection to load services or repositories from another module.

Example:

   use Modules\Billing\Services\InvoiceService;

   class OrderService {
       protected $invoiceService;

       public function __construct(InvoiceService $invoiceService) {
           $this->invoiceService = $invoiceService;
       }

       public function createOrder($data) {
           // Communicate with another module, e.g., Billing
           $this->invoiceService->createInvoice($data);
       }
   }
Enter fullscreen mode Exit fullscreen mode

In this approach, you are using the service from one module inside another module by injecting it into the service class or controller.

2. Event-Driven Communication

  • An event-driven approach can help decouple modules. One module can dispatch an event, and another module can listen to that event and handle it accordingly.

Example:

  • Event Dispatching (from Module A):

     use App\Events\OrderCreated;
    
     class OrderService {
         public function createOrder($data) {
             // Order creation logic
    
             // Dispatch event
             event(new OrderCreated($data));
         }
     }
    
  • Event Listener (in Module B):

     use App\Events\OrderCreated;
    
     class InvoiceEventListener {
         public function handle(OrderCreated $event) {
             // Create an invoice when an order is created
             InvoiceService::createInvoice($event->data);
         }
     }
    

This makes the modules less dependent on each other and promotes a more decoupled design.

3. Service Providers and Facades

  • You can create a custom Service Provider in each module to register services and bind them to the Laravel Service Container. Facades can be used to provide an easy way to access services across modules.

Example:

  • In Module A, you could have a service provider:

     namespace Modules\Billing\Providers;
    
     use Illuminate\Support\ServiceProvider;
     use Modules\Billing\Services\InvoiceService;
    
     class BillingServiceProvider extends ServiceProvider {
         public function register() {
             $this->app->singleton(InvoiceService::class, function ($app) {
                 return new InvoiceService();
             });
         }
     }
    
  • In Module B, you can use the service by resolving it from the service container:

     $invoiceService = app(InvoiceService::class);
     $invoiceService->createInvoice($data);
    

4. Repository Pattern for Cross-Module Communication

  • If each module contains a repository layer for data access, you can inject the repository of one module into the service of another module.

Example:

   use Modules\Billing\Repositories\InvoiceRepository;

   class OrderService {
       protected $invoiceRepository;

       public function __construct(InvoiceRepository $invoiceRepository) {
           $this->invoiceRepository = $invoiceRepository;
       }

       public function createOrder($data) {
           // Use repository from another module
           $this->invoiceRepository->save($data);
       }
   }
Enter fullscreen mode Exit fullscreen mode

5. Use Laravel Jobs/Queues

  • For decoupled communication, especially for time-consuming tasks, one module can dispatch a job that will be handled by another module in the background.

Example:

   use Modules\Billing\Jobs\CreateInvoiceJob;

   class OrderService {
       public function createOrder($data) {
           // Dispatch job to another module
           CreateInvoiceJob::dispatch($data);
       }
   }
Enter fullscreen mode Exit fullscreen mode

Each of these approaches has different levels of decoupling and complexity, so your choice will depend on the needs of your application and whether you prefer stronger boundaries between modules or simpler integration methods.

Do your career a big favor. Join DEV. (The website you're on right now)

It takes one minute, it's free, and is worth it for your career.

Get started

Community matters

Top comments (1)

Collapse
 
albertoguarise profile image
Alberto

Great article! I have a question: how would you handle those rare cases where a "JOIN" of data from two modules is needed?

One approach could be to use a direct call and merge the data in memory. However, in my opinion, this is not very efficient, especially if you need to manage pagination with filters.

I imagine this kind of requirement would arise in analytical APIs, for example, a view that displays modules like Events, Tickets, and Locations.

Do you have any ideas or suggestions on how to approach this?

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Discover a treasure trove of wisdom within this insightful piece, highly respected in the nurturing DEV Community enviroment. Developers, whether novice or expert, are encouraged to participate and add to our shared knowledge basin.

A simple "thank you" can illuminate someone's day. Express your appreciation in the comments section!

On DEV, sharing ideas smoothens our journey and strengthens our community ties. Learn something useful? Offering a quick thanks to the author is deeply appreciated.

Okay