Een order wordt geplaatst. Tegelijk moet de voorraad worden gereserveerd, een bevestigingsmail verstuurd, de boekhouding bijgewerkt en het ERP gesynchroniseerd. Event-driven architectuur maakt dit schaalbaar.
Event-driven architectuur in Laravel — orders, voorraad, notificaties
Een order wordt geplaatst. Op dat moment moeten 6 dingen tegelijk gebeuren: voorraad reserveren, bevestigingsmail sturen, boekhouding bijwerken, ERP synchroniseren, accountmanager notificeren, fraudecheck uitvoeren.
Als je dit allemaal in één controller method zet, heb je binnen drie maanden een functie van 200 regels die niemand meer durft aan te raken. Event-driven architectuur lost dit op door elke verantwoordelijkheid te scheiden.
Wat je leert in dit artikel
- Hoe events en listeners werken in Laravel
- Concrete e-commerce use cases: orders, voorraad, notificaties
- Observers voor Eloquent model events
- Broadcasting voor real-time updates in dashboards
- Queued listeners voor langzame operaties
- Event sourcing als volgende stap
Het principe
Een event is een feit dat heeft plaatsgevonden. OrderPlaced, PaymentConfirmed, StockDepleted. Het event weet niet wat er daarna gebeurt. Het meldt alleen dat iets is gebeurd.
Listeners reageren op events. Ze zijn onafhankelijk van elkaar. Je kunt een listener toevoegen of verwijderen zonder de code te veranderen die het event dispatcht.
OrderController::store()
→ dispatch(new OrderPlaced($order))
→ SendOrderConfirmationEmail (queued)
→ ReserveInventory (sync)
→ NotifyAccountManager (queued)
→ SyncToErp (queued)
→ CheckForFraud (queued)
De controller hoeft niet te weten wat er met de order gebeurt. Hij meldt alleen dat een order is geplaatst.
Events definiëren
// app/Events/OrderPlaced.php
namespace App\Events;
use App\Models\Order;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class OrderPlaced
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(
public readonly Order $order,
public readonly string $source = 'web', // 'web', 'api', 'import'
) {}
}
// app/Events/PaymentConfirmed.php
class PaymentConfirmed
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(
public readonly Order $order,
public readonly string $paymentMethod,
public readonly float $amount,
public readonly string $transactionId,
) {}
}
// app/Events/StockDepleted.php
class StockDepleted
{
use Dispatchable, SerializesModels;
public function __construct(
public readonly \App\Models\Product $product,
public readonly int $warehouseId,
) {}
}
Listeners registreren
// app/Providers/EventServiceProvider.php
protected $listen = [
\App\Events\OrderPlaced::class => [
\App\Listeners\ReserveInventory::class, // Sync — moet direct
\App\Listeners\SendOrderConfirmationEmail::class, // Async — mag wachten
\App\Listeners\NotifyAccountManager::class, // Async
\App\Listeners\SyncOrderToErp::class, // Async
\App\Listeners\CheckOrderForFraud::class, // Async
],
\App\Events\PaymentConfirmed::class => [
\App\Listeners\UpdateOrderStatus::class,
\App\Listeners\TriggerFulfillment::class,
\App\Listeners\SendPaymentReceiptEmail::class,
\App\Listeners\UpdateAccountingSystem::class,
],
\App\Events\StockDepleted::class => [
\App\Listeners\NotifyPurchasingDepartment::class,
\App\Listeners\HideProductFromStorefront::class,
\App\Listeners\CreateReplenishmentOrder::class,
],
];
Listeners implementeren
Synchrone listeners — voor operaties die direct moeten plaatsvinden:
// app/Listeners/ReserveInventory.php
namespace App\Listeners;
use App\Events\OrderPlaced;
use App\Services\InventoryService;
class ReserveInventory
{
public function __construct(private InventoryService $inventoryService) {}
public function handle(OrderPlaced $event): void
{
foreach ($event->order->lines as $line) {
$this->inventoryService->reserve(
productId: $line->product_id,
quantity: $line->quantity,
orderId: $event->order->id,
);
}
}
}
Asynchrone listeners — voor operaties die mogen wachten:
// app/Listeners/SyncOrderToErp.php
namespace App\Listeners;
use App\Events\OrderPlaced;
use App\Services\ErpSyncService;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
class SyncOrderToErp implements ShouldQueue
{
use InteractsWithQueue;
// Welke queue gebruiken
public string $queue = 'erp-sync';
// Maximaal 3 pogingen bij falen
public int $tries = 3;
// Wacht 60 seconden voor herpoging
public int $backoff = 60;
public function __construct(private ErpSyncService $erpService) {}
public function handle(OrderPlaced $event): void
{
$this->erpService->pushOrder($event->order);
}
// Wat te doen als alle pogingen mislukken
public function failed(OrderPlaced $event, \Throwable $exception): void
{
\Log::error('ERP sync mislukt na 3 pogingen', [
'order_id' => $event->order->id,
'error' => $exception->getMessage(),
]);
// Notificeer het team
\Notification::route('slack', config('services.slack.ops_webhook'))
->notify(new ErpSyncFailed($event->order, $exception));
}
}
Observers voor Eloquent model events
Observers zijn een specialisatie van het event patroon voor Eloquent models. Ze luisteren naar database events: creating, created, updating, updated, deleting, deleted.
// app/Observers/OrderObserver.php
namespace App\Observers;
use App\Events\OrderPlaced;
use App\Events\OrderStatusChanged;
use App\Models\Order;
class OrderObserver
{
public function created(Order $order): void
{
// Dispatch het event zodra de order in de database staat
OrderPlaced::dispatch($order);
}
public function updated(Order $order): void
{
// Alleen dispatchen als de status daadwerkelijk is veranderd
if ($order->wasChanged('status')) {
OrderStatusChanged::dispatch(
order: $order,
oldStatus: $order->getOriginal('status'),
newStatus: $order->status,
);
}
}
public function deleted(Order $order): void
{
// Reservering vrijgeven bij het verwijderen van een order
foreach ($order->lines as $line) {
\App\Events\InventoryReservationReleased::dispatch($line);
}
}
}
// app/Providers/AppServiceProvider.php
public function boot(): void
{
Order::observe(OrderObserver::class);
Product::observe(ProductObserver::class);
}
Broadcasting voor real-time dashboards
Events kunnen ook naar de browser worden gestuurd via WebSockets. Accountmanagers zien nieuwe orders zonder de pagina te verversen.
// app/Events/OrderPlaced.php — broadcasting toevoegen
class OrderPlaced implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(public readonly Order $order) {}
public function broadcastOn(): array
{
return [
// Privékanaal voor de accountmanager van de klant
new PrivateChannel("account-manager.{$this->order->customer->account_manager_id}"),
// Adminkanaal voor alle orders
new PrivateChannel('admin.orders'),
];
}
public function broadcastWith(): array
{
// Stuur alleen de data die de frontend nodig heeft
return [
'order_id' => $this->order->id,
'reference' => $this->order->reference,
'customer_name' => $this->order->customer->name,
'total' => $this->order->total,
'created_at' => $this->order->created_at->toISOString(),
];
}
// Naam van het event in de frontend
public function broadcastAs(): string
{
return 'order.placed';
}
}
// Frontend: luisteren naar nieuwe orders
Echo.private(`account-manager.${accountManagerId}`)
.listen('.order.placed', (data) => {
// Voeg order toe aan de tabel zonder pagina te herladen
orderTable.prepend(data);
showNotification(`Nieuwe order: ${data.reference}`);
});
Concrete e-commerce flows
Voorraad beheer flow
StockReceived
→ UpdateInventoryLevels (sync)
→ ReactivateOutOfStockProducts (queued)
→ NotifyWaitingCustomers (queued)
→ UpdateSearchIndex (queued)
StockDepleted
→ HideProductFromStorefront (sync)
→ NotifyPurchasingDepartment (queued)
→ CancelBackorders (queued)
Betaling flow
PaymentInitiated
→ CreatePendingPaymentRecord (sync)
→ LogPaymentAttempt (queued)
PaymentConfirmed
→ UpdateOrderStatus (sync)
→ TriggerFulfillment (queued)
→ SendReceiptEmail (queued)
→ UpdateAccounting (queued)
PaymentFailed
→ UpdateOrderStatus (sync)
→ NotifyCustomer (queued)
→ ReleaseInventoryReservation (queued)
→ LogFailedPayment (queued)
Verzending flow
OrderShipped
→ UpdateOrderStatus (sync)
→ SendTrackingEmail (queued)
→ NotifyAccountManager (queued)
→ UpdateErp (queued)
OrderDelivered (webhook van vervoerder)
→ UpdateOrderStatus (sync)
→ TriggerReviewRequest (queued, delay 3 dagen)
→ UpdateCustomerMetrics (queued)
Veelgemaakte fouten
- Listeners te zwaar maken — Een listener doet één ding. Te veel logica in een listener maakt het moeilijk te testen. Delegeer zware logica naar services.
- Sync listeners voor trage operaties — API calls naar ERP, e-mail versturen, PDF genereren — dit doet altijd async. Sync listeners verlengen de response tijd van je controller.
- Events niet loggen — Bij een mislukte ERP sync wil je weten welke order niet is gesynchroniseerd. Log events bij critical operaties.
- Queue workers vergeten te herstarten na deploy — Queued listeners worden in memory geladen. Na een deploy draaien workers nog met de oude code. Altijd
php artisan queue:restartbij deploy.
# In je deploy script
php artisan queue:restart
php artisan horizon:terminate # Als je Horizon gebruikt
Event sourcing als volgende stap
Als je alle events opslaat in plaats van alleen de huidige state, heb je event sourcing. Je kunt dan de volledige ordergeschiedenis reconstrueren, terug in de tijd kijken en historische rapporten genereren.
Voor de meeste e-commerce projecten is dit overkill. Maar voor platforms met complexe auditbehoeften — financiële diensten, medische supplies, overheidsklanten — is het de moeite waard te verkennen via het spatie/laravel-event-sourcing package.
Conclusie
Event-driven architectuur maakt je e-commerce backend modulair en uitbreidbaar. Een nieuwe functie toevoegen betekent een nieuwe listener schrijven, niet een bestaande controller aanpassen. Dat is het verschil tussen code die groeit en code die verstikt.
Begin met de flows die de meeste koppelingen hebben: order aanmaken, betaling bevestigen, verzending. De rest volgt vanzelf als het patroon eenmaal zit.
Laravel's Events en Listeners documentatie en het Spatie Event Sourcing package zijn de referenties voor de patronen in dit artikel. Het Observer pattern is de basis van event-driven architectuur.
Gerelateerde artikelen:- Laravel Queues voor e-commerce — asynchrone orderverwerking
- Laravel als e-commerce middleware — het complete architectuurplaatje
- Laravel API design patterns — API's die samenwerken met events
- Shopify + Picqer koppeling — events in de praktijk
Bekijk onze Laravel-diensten of neem contact op voor een architectuurgesprek.

Geschreven door Ruthger Idema
15+ jaar ervaring in e-commerce development. Gespecialiseerd in Magento, Shopify en Laravel maatwerk.
Meer over ons team →