Implementing Event-Driven Architecture for Async PHP SaaS AI Applications

This article shows how to use Laravel’s event‑driven architecture—events, listeners, and queue jobs—to decouple slow AI calls in a SaaS product, delivering instant HTTP responses, background processing, real‑time WebSocket updates, robust retry/failure handling, and fast, isolated testing.

Open Source Tech Hub
Open Source Tech Hub
Open Source Tech Hub
Implementing Event-Driven Architecture for Async PHP SaaS AI Applications

When a user submits a complex prompt to an AI chat application, waiting 15 seconds for a response creates a poor experience. Treating the AI call as a normal HTTP request blocks the web server, load balancer, and browser, and can trigger time‑outs because large‑model calls may take 2–30 seconds.

Why Event‑Driven Architecture (EDA) solves the problem

EDA replaces synchronous waiting with message‑based communication. In Laravel an Event notifies interested Listeners , which can dispatch a Queue Job that runs independently of the original HTTP request. This decouples the slow AI operation from the user‑facing request.

Core components

Event : a notification that something happened, e.g. PromptSubmitted or AiResponseGenerated.

Listener : code that reacts to a specific event; one event may have many listeners.

Queue Job : a background task that performs the heavy AI call without blocking the request.

Parallel execution flows

Synchronous flow (fast response): the HTTP request writes the prompt to the database, fires PromptSubmitted, and immediately returns a 201 response so the UI does not wait for the AI.

Asynchronous flow (background processing): the PromptSubmitted event triggers a listener ( DispatchAiProviderJob) that dispatches CallAiProviderJob. The job calls the AI provider (2–30 s), then fires AiResponseGenerated. Three listeners handle the generated response: store it in the database, broadcast it via WebSocket, and record token usage for billing.

Code implementation

Step 1 – Define the event :

namespace App\Domain\Chat\Events;

use App\Domain\Chat\Models\Conversation;
use App\Domain\Chat\Models\Message;

class PromptSubmitted {
    public function __construct(public readonly Conversation $conversation, public readonly Message $message) {}
}

Step 2 – Listener that dispatches the queue job :

namespace App\Domain\Chat\Listeners;

use App\Domain\Chat\Events\PromptSubmitted;
use App\Domain\Chat\Jobs\CallAiProviderJob;

class DispatchAiProviderJob {
    public function handle(PromptSubmitted $event): void {
        CallAiProviderJob::dispatch($event->conversation, $event->message);
    }
}

Step 3 – Queue job that calls the AI provider (implements ShouldQueue and defines retry/back‑off):

namespace App\Domain\Chat\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use App\Domain\Chat\Models\Conversation;
use App\Domain\Chat\Models\Message;
use App\Domain\Chat\Events\AiResponseGenerated;

class CallAiProviderJob implements ShouldQueue {
    public int $tries = 3;
    public int $backoff = 5;

    public function __construct(public readonly Conversation $conversation, public readonly Message $message) {}

    public function handle(AiProviderRouterService $router): void {
        $provider = $router->resolveFor($this->conversation->tenant);
        $response = $provider->complete(conversation: $this->conversation, prompt: $this->message);
        event(new AiResponseGenerated($this->conversation, $response));
    }
}

Step 4 – Multiple listeners for the AI response event (store, broadcast, billing):

// Save to DB
class SaveAiResponseToDatabase {
    public function handle(AiResponseGenerated $event): void {
        $event->conversation->messages()->create([
            'role' => 'assistant',
            'content' => $event->response->content,
        ]);
    }
}

// Broadcast via WebSocket
class BroadcastAiResponseToFrontend {
    public function handle(AiResponseGenerated $event): void {
        broadcast(new AiResponseReady($event->conversation->id, $event->response->content))->toOthers();
    }
}

// Record token usage for billing
class RecordTokenUsageForBilling {
    public function handle(AiResponseGenerated $event): void {
        RecordUsageAction::run($event->conversation->tenant, $event->response->tokenUsage);
    }
}

Failure handling

Laravel’s queue system provides automatic retries. The job distinguishes unretryable errors (e.g., content‑policy violations) from transient ones (e.g., time‑outs). Unretryable errors call $this->fail($e), while time‑outs are re‑thrown to let the queue retry. After all retries are exhausted, a AiResponseFailed event notifies the user.

Testing without real AI calls

Using Event::fake() and Bus::fake() prevents actual listeners and jobs from running. Tests can assert that the correct events and jobs are dispatched and that they carry the expected data (prompt content, token usage) without contacting an external AI service.

Common pitfalls

Attaching too many listeners to a single event makes the flow hard to trace; consolidate related logic.

Ignoring idempotency in retryable jobs can create duplicate records; use updateOrCreate instead of create where appropriate.

Failing to notify the user after a job gives up leads to a silent dead‑end.

Using events for strictly ordered processes; if later steps depend on earlier results, keep the sequence in a single action rather than separate events.

Directory layout (example)

app/
  Domain/
    Chat/
      Events/
        PromptSubmitted.php
        AiResponseGenerated.php
        AiResponseFailed.php
      Listeners/
        DispatchAiProviderJob.php
        SaveAiResponseToDatabase.php
        BroadcastAiResponseToFrontend.php
        RecordTokenUsageForBilling.php
      Jobs/
        CallAiProviderJob.php

Conclusion

If you can adopt only one architecture from the series, choose event‑driven architecture. It keeps the user interface responsive while heavy AI processing runs in the background, and it integrates naturally with Laravel’s testing tools and queue‑based retry mechanisms.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

aiwebsocketphpasyncsaasevent-drivenqueuelaravel
Open Source Tech Hub
Written by

Open Source Tech Hub

Sharing cutting-edge internet technologies and practical AI resources.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.