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 (7)
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.
I think we’re talking past each other a bit. Let me clarify what my post was really aiming for.
There are two categories of work in this flow:
Database operations that must succeed together – creating the store, assigning the admin role, attaching abilities, and adding default brands/categories/products. I am treating these as events.
Side effects that must succeed for the process to be “complete” – in this case, sending notification emails.
This setup is intentional:
If sending an email fails, I want the whole thing rolled back. For this use case, the store isn’t “ready” unless both the database records and the emails are successfully completed. Yes, email adds a little extra time, but in this flow it must be synchronous, because delivering the email is part of the business transaction itself.
From the UX side: the user sees “operation is being processed,” and once the emails go out, a notification confirms completion. That way I don’t leave them with a half-done setup (store exists in the DB but no one was notified).
Another option we have is to split the main event to two different events and call the second one having emails sending listeners after commit.
Ok I read over this at least four times until now. That could saved us some time.
By moving the code to a listener you make it public. Is it the intention that the code can be used by other parts of the application, because that is one of the consequences of making the code public.
Queries like assign user to store (as admin) are no problem because they are generic.
But code like add abilities to user for a store I would keep private because it is likely that it contains logic that is only valid when creating a store.
I think the best solution is a mix of events and encapsulated methods.
Adding the send email code seems out of place in a class or as a listener, because it has nothing to do with the database storage.
I understand wanting to use the transaction, but there are other ways to do a rollback. Just use the store id to remove the rows in the affected tables. If you set up the foreign keys correctly it shouldn't be that much of an effort.
To summarize, don't go all in on events because you just discovered them.
PS: the chain of responsibility is a bad choice in this use case.