Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Async Service Call Bundle Laravel Package

krlove/async-service-call-bundle

A Symfony bundle for running service calls asynchronously, letting you defer heavy work to background processes and return control immediately. Useful for improving response times when handling slow tasks like emails, API calls, or reports without blocking the request.

View on GitHub
Deep Wiki
Context7

Getting Started

Read the source, not the README. The repository has no meaningful adoption and the docs are thin. Before you do anything, open the bundle and find three classes: the proxy/interceptor (how a method call becomes a message), the dispatcher (how the message is sent), and the handler/resolver (how the worker invokes the original method). That trio is the contract. Everything else is Symfony wiring you will not reuse.

Confirm the framework mismatch first. This is a Symfony bundle — it registers via config/bundles.php, hooks the Symfony DI container, and relies on Symfony service tags and compiler passes. There is no composer require path into a Laravel application. Your first actionable step is a 30-minute decision: are you (a) in a Symfony codebase and the "Laravel developer" framing is incidental, or (b) actually on Laravel, in which case you are reading this as a reference design and reproducing the pattern natively. Be honest with yourself here; most day-to-day "leverage" in a Laravel shop means option (b).

First use case. Pick a call site that is observably slow, non-critical, and idempotent. Good first candidates: outbound webhook fan-out, notification/email dispatch trigger, analytics/audit event emission, cache warming, third-party API enrichment that does not block the response body. Bad first candidates: anything that touches money, inventory, or user-visible state that must be consistent on the next page load.

Laravel-native minimal path (the realistic one). If you're on Laravel, the first usable thing you build looks like this:

  1. Create a job per proxied method (SendWelcomeEmailJob, NotifyWebhookJob).
  2. Create a thin decorator around your service that implements the same interface and calls dispatch(new ...Job(...)) instead of the real method.
  3. Bind the decorator in a ServiceProvider, but only for services on your allow-list — never globally.
  4. Return a typed handle (a small value object, e.g. PendingCall) rather than a fake return value, so callers cannot mistake the call for synchronous.

That's the whole first use case. It takes a few hours and gives you 80% of what the Symfony bundle does.

Implementation Patterns

The decorator-plus-job pattern (primary workflow). Bind the original service interface to a decorator that dispatches a job and returns a PendingCall/PendingDispatch handle. Keep the decorator thin — it should only translate a call into a job, not carry business logic. If a proxied method returns a value, decide up front: either the method is fire-and-forget (drop the return, document it loudly), or you collect results via a results table or Bus::batch() callback. Never silently return null in place of a real return value.

Attribute or comment annotation for discoverability. If you want the Symfony "attribute" ergonomics, add your own #[Async] attribute to the proxied method and have a container resolver detect it. This is optional, but it keeps the async boundary visible to a reader scanning the service — which the proxy pattern otherwise hides.

One queue, one workload class, one connection. Give async side effects their own queue name and worker pool (queue:work --queue=async-side-effects). Do not share the default queue with user-facing critical jobs, because a slow async call will starve your real mailer. Configure a dedicated connection per workload class if throughput profiles differ.

Trace propagation is non-negotiable. When the caller invokes the decorated method, capture the request's trace ID (or generate one) and pass it into the job payload. Add a Bus::pipeThrough() middleware or a base Job class that stamps it into logs. Without this, the first incident is a multi-hour archaeology dig through worker logs with no link back to the request.

Idempotency by design. Assume every job runs twice. Enforce idempotency with a dedupe key column, ShouldBeUnique on the job, or a natural business key. This is the pattern that separates "async" from "flaky."

Result collection via batches or polling — decide the shape early. If the caller sometimes needs the result, prefer Bus::batch() with .then() for fan-in, or a async_call_results table polled by a handle method with a timeout. Avoid blocking on a handle inside a web request — that reintroduces the latency you removed.

Retries and failure handling via Laravel primitives. Set $tries, $backoff, $maxExceptions, and a failed() method on every async job. Route exhausted jobs to a dead-letter path (failed_jobs table or a dedicated failed-async queue) and alert on depth/age. The Symfony bundle delegates this to Messenger middleware; in Laravel you delegate it to the queue driver and Horizon (or whatever you run).

Mix Symfony and Laravel only at a message boundary. If you genuinely have both stacks and must use the bundle, do it in the Symfony service and communicate to Laravel via a queue/message contract (AMQP, Redis streams, SQS). Do not try to share a container, a service locator, or a "unified" proxy — that is a long-term architectural tax that is not justified by this bundle's adoption signal.

Opt-in, interface-bound, allow-listed. The safest integration is: App\Contracts\FooService → bound to AsyncFooService only in environments/feature flags where the allow-list includes this service. Provide a synchronous binding behind a config toggle so you can revert a call site by flipping one config flag during an incident.

Gotchas and Tips

You cannot install this in Laravel. It is a Symfony bundle. Any attempt to composer require it into a Laravel app will either fail or produce a dependency you cannot wire. Leverage it as a specification for your own implementation, not as a dependency. If you actually adopt it anywhere (i.e., in a Symfony sidecar), pin an exact version, vendor the code, and write an exit-path ticket the same week — with zero stars and zero dependents, you are the maintainer.

Async is invisible and that is the bug. A proxy that silently makes a method async is the single fastest way to erode team trust in the pattern. Make the handle type visually distinct (PendingCall, DeferredResult) and never let a proxied method share a return type with the original. If the method used to return Invoice, the proxied version must return PendingCall<Invoice> or nothing at all.

Silent loss is the default failure mode. Worker dies mid-job → caller never knows. Mitigations are cheap and mandatory: failed_jobs monitoring, alerting on queue depth and oldest-job age, and a failed() method that pages if the workload is anything the business cares about. If the workload is not loss-tolerant, do not defer it — this is a hard line, not a tuning knob.

Exceptions surface out-of-band. A TypeError thrown inside a worker is not caught by your controller's try/catch and will not appear in the HTTP error handler. Test the "job fails" path explicitly, not just the happy path. Log the exception inside failed() with the trace ID.

Arguments must serialize. Closures, service instances, Request objects, Eloquent models with unsaved relations, and resources will not survive the queue payload. Enforce scalar/DTO payloads at the decorator boundary: the decorator's job is to reduce the arguments to serializable primitives (IDs, arrays, DTOs) before dispatch. This catches the vast majority of production bugs in this pattern.

dispatchAfterResponse is not a substitute for a queue. It runs in the same PHP process after the response is flushed — it will not survive process death and does not give you retries or a worker pool. Fine for tiny fire-and-forget logging; never for anything you'd want to know failed.

The sync driver will ruin your benchmarking. Locally and in CI, QUEUE_CONNECTION=sync runs jobs inline — so your "async" code looks synchronous and tests pass that would fail in production. Either run a real queue in CI or explicitly test both drivers. Add a check that raises in non-production if an async-bound service is dispatched on sync in a context where you expect deferral.

Fan-out amplification. A proxied call that used to run once per request now enqueues once per request. If that request is hot, you have just multiplied queue depth by request rate. Model this before the pilot: what is the peak dispatch rate, what is the worker concurrency, what is the worst-case backlog age? If the answer is "unbounded," you need a broker with backpressure, not a decorator.

Ordering is gone. Concurrent workers reorder side effects that callers implicitly assumed were sequential — emails arrive out of order, webhooks fire in reverse, audit records appear shuffled. If a flow depends on order, add an ordering key (per-entity queue partitioning, or a chained job) or keep that flow synchronous.

Static analysis, IDE navigation, and stack traces will fight you. Any proxy layer that intercepts method calls confuses phpstan/psalm, IDEs cannot jump to the real implementation, and stack traces gain a layer of indirection. Budget for this: keep proxies explicit and few, annotate with @method where helpful, and require a trace ID in every log line on both sides of the boundary so debugging does not depend on the call graph.

Extension points worth knowing (Laravel side). Job::middleware() handles cross-cutting concerns (tracing, idempotency guards, throttling). Bus::pipeThrough() applies them globally. ShouldBeUnique / ShouldBeUniqueUntilProcessing give you dedupe. retryUntil() gives you a deadline. RateLimited middleware gives you throttling per key. Compose these rather than writing bespoke retry logic — the queue driver already solves most of what an async-service-call framework sets out to solve.

A written "when not to use this" page pays for itself. Half a day of team enablement, plus a short doc listing your allow-listed services and the reasons they're safe to defer, is the difference between a useful internal pattern and a landmine. Include the exit path: "to make this synchronous again, flip feature flag async.<service> to false."

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core