Many times while building Laravel applications, I ran into situations where a single process required handling multiple tasks. At first, I wrote everything inside service classes, but those classes quickly became too long and messy. Everything ended up wrapped inside one big transaction, and testing became painful because all the logic was tightly packed into the same place. That’s when I realized I needed a better approach. So I started breaking methods into separated classes. This approach was much better but when going through the Laravel Docs I found a better solution if used efficiently, our code becomes cleaner, more maintainable, and far easier to test.
Laravel Events
When you need to run a series of actions to complete a process, Laravel Events are the right tool. An event can trigger multiple listeners, and each listener is an independent class that doesn’t depend on the others. This keeps the workflow smooth, decoupled, and easy to extend. Instead of cramming everything into one oversized service class, you can break tasks into small, testable units that can be added, removed, or modified without touching the rest. By using events effectively, each part of your process remains clean, isolated, and much easier to manage as your application grows.
Example: E-Commerce – Store Initialization
Let’s say you’re building an e-commerce SaaS platform. A user comes in, registers, and creates their store.
When user presses Save button the following happens behind the scene.
- Save the store.
- Assign store to current authenticated user.
- Assign admin role to the user.
- Assign default permissions to that admin.
- Create default brands, categories, and products.
- Send an email to the admin telling: "Your store is ready."
- Send an email to the super admin telling"A new store has been registered".
Now think:
Would you put all this in the controller? In a service? In a job?
No. Just fire an event:
event(new StoreCreated($store));
This will trigger all the actions that are required to complete this process. Here's how we do it.
php artisan make:event StoreCreated
This will generate the following class in app/Events
folder.
<?php
namespace App\Events;
use App\Models\Store;
use Illuminate\Foundation\Events\Dispatchable;
class StoreCreated
{
use Dispatchable;
/**
* Create a new event instance.
*/
public function __construct(
public Store $store,
) {}
}
Next we need to create listeners for this event.
php artisan make:listener AssignStore --event=StoreCreated
php artisan make:listener AssignAdminRole --event=StoreCreated
php artisan make:listener AssignAdminAbilities --event=StoreCreated
php artisan make:listener AddDefaultBrands --event=StoreCreated
php artisan make:listener AddDefaultCategories --event=StoreCreated
php artisan make:listener AddDefaultProducts --event=StoreCreated
php artisan make:listener SendStoreReadtEmail --event=StoreCreated
php artisan make:listener SendNewStoreCreatedEmail --event=StoreCreated
Register Listeners: The New Way
If we create a listener, Laravel will automatically scan the `Listeners`
directory and register it for us. When we define a listener method like `handle`
and type-hint the event in its signature, Laravel knows which event it should respond to. For example, if you run:
php artisan make:listener AssignStore --event=StoreCreated
Laravel will generate the listener and link it to the `StoreCreated`
event automatically.
Note: You can also create directory inside listeners directory to group all the listeners that are required to be processed togather. For example to keep it simple use the same name as event to create a directory.
app/
└── Listeners/
└── StoreCreated/
└── AssignStore.php
└── AssignAdminRole.php
└── AssignAdminAbilities.php
└── AddDefaultBrands.php
└── AddDefaultCategories.php
└── AddDefaultProducts.php
└── SendStoreReadtEmail.php
└── SendNewStoreCreatedEmail.php
And our command will look like this.
php artisan make:listener StoreCreated/AssignStore --event=StoreCreated
My Customization
But if you want to prefer the old way just (as i do) you can do it like this.
php artisan make:provider EventServiceProvider
This will generate EventServiceProvider
class in app/Providers
folder.
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class EventServiceProvider extends ServiceProvider
{
/**
* Register services.
*/
public function register(): void
{
//
}
/**
* Bootstrap services.
*/
public function boot(): void
{
//
}
}
Now we can create $listen
property and do the stuff.
protected $listen = [
StoreCreated::class => [
AssignStore::class
AssignAdminRole::class
AssignAdminAbilities::class
AddDefaultBrands::class
AddDefaultCategories::class
AddDefaultProducts::class
SendStoreReadtEmail::class
SendNewStoreCreatedEmail::class
],
];
If we do this we are free to place our classes anywhere in the application like services
, helpers
or AQC
. Just we have to define handle()
method and type-hint
the event class into it.
Finally we can call the event after we have saved the store data. I usually prefer to do it in Observer class.
<?php
namespace App\Observers;
use App\Models\Store;
use App\Events\StoreCreated;
class StoreObserver
{
public function created(Store $store)
{
event(new StoreCreated($store));
}
}
Final Thoughts
When building scalable Laravel applications, it’s easy to fall into the trap of overloading controllers or service classes with too much responsibility. Events and listeners provide a clean and elegant way to separate concerns while keeping your codebase flexible. By splitting processes into small, independent listeners, you gain testability, maintainability, and the ability to extend workflows without fear of breaking existing logic. Whether you stick to Laravel’s automatic event discovery or prefer the old-school `EventServiceProvider`
mapping, the key is consistency. Once you start embracing events, you’ll find that your code naturally evolves into a system that’s easier to reason about, easier to test, and far more adaptable to future requirements.
If you found this post helpful, consider supporting my work — it means a lot.
Top comments (5)
Events are not always the right solution. In the case of the example you need to have confirmation that the store is saved, that the user is made admin of the store and that the permissions are set. And those actions should be run sequential.
Events don't provide a way to notify you when an event action failed or not. You have to write the outcome somewhere so it can be fetched.
Eventlisteners wil run out of the box in the order added, but that is not guaranteed. You should think of eventlisteners as parallel running processes.
The pattern to use for the actions I mentioned is the Chain of responsibility pattern. In Laravel that pattern is used for the routing middleware.
The simplest implementation is
Because one class calls another the chain is not changeable in the controller. So this should be used in case the chain only changes once in a blue moon.
When chain changes more often create a way to make the chain configurable.
I see what you’re saying, but I don’t agree that the Chain of Responsibility pattern is the right fit here. What you’re describing with separate action classes returning error objects is basically reinventing what Laravel already gives us with transactions and exceptions.
In my example, all three steps (store creation, admin assignment, extra permissions) can safely run inside a database transaction. If something fails, the transaction rolls back automatically, and I don’t need to wrap each operation in a custom Error return. That’s simpler, less boilerplate, and leverages the framework instead of building a mini-framework on top of it.
Events weren’t meant to guarantee persistence, they’re for reacting after the business logic has succeeded. For the “must succeed in sequence” part, transactions and exceptions are more natural. Adding a Chain of Responsibility here feels like ceremony without a real payoff.
That is not how events work. You can't have three listeners; AssignStore, AssignAdminRole, AssignAdminAbilities, and have a single database transaction. I'm basing my code and assumptions on your example.
You still need to know which part of the transaction failed, and that means you need to check the generic database transaction error.
And that is a lot more error prone than creating custom errors from the start. My example could be improved by creating specific error classes instead of a generic error class.
Calling it a mini-framework is pushing it, this is one extra layer of abstraction. Your AQC pattern does the same thing for different reasons.
So we agree that the actions/steps should be in sequence. That is the reason you shouldn't use listeners. How are you going to get the store id when AssignStore and AssignAdminRole are not aware of each other?
The chain of responsibility pattern is a concept to describe one action can be triggered after another action is done, and this can become a chain. The examples are just implementations. You can dispatch an event in a listener to create a chain, and there are probably more implementations.
You’re mixing persistence with side effects. The store creation, admin role, and abilities aren’t “events”—they’re domain operations that should live inside a transaction. Laravel’s transactions + exceptions already give you sequential execution, rollback, and a clear failure point without wrapping every step in custom error objects.
Events make sense after the transaction succeeds (emails, logging). Listeners aren’t supposed to depend on each other, so chaining them just to pass IDs around is misuse.
And unit testing is way easier this way: one service/action class, one test, assert rollback on failure. No need to mock a bunch of listeners or juggle custom errors to prove something the database and exceptions handle for free.
Are you saying the three listeners; AssignStore, AssignAdminRole, AssignAdminAbilities from your post don't execute a query?
Can you show where I mentioned those actions are events?
The post isn't about transactions, so why do you keep on hammering on the one functionality that has nothing to do with the post. You just want to steer the conversation in another direction.
You never created fail events?
Why is it misuse? Event-driven programming is build on chaining events.
There is no reason to mock listeners, they can be tested on their own. Chaining doesn't change how listeners work.
The only extra thing you need to test is the dispatching of an event. How that event is handled is out of scope.