micro-module/fault-tolerance
Fault tolerance utilities for Laravel micro-modules: add retries, circuit breakers, timeouts, and graceful fallbacks around service calls so failures in one module don't cascade. Configurable policies and framework-native integration keep resilient code simple.
composer require micro-module/fault-tolerance
Then, in order:
src/ and look for the service provider (FaultToleranceServiceProvider or similar), the config file (config/fault-tolerance.php), and any facade or manager class. Those three files tell you the entire public surface area in about ten minutes.php artisan vendor:publish --provider="MicroModule\\FaultTolerance\\FaultToleranceServiceProvider"
If no publish tag exists, copy the config file manually and register it — the package is new enough that packaging conventions may not be fully baked.php artisan about and check the providers list, or php artisan tinker and resolve the main class from the container. Given opportunity: 27.8 and no dependents, auto-discovery may or may not be wired correctly.The canonical entry point for a fault-tolerance library is wrapping an outbound call to a sibling microservice — payment, inventory, notifications — that you don't control. Start with the single flakiest downstream dependency in your system and wrap it:
use MicroModule\FaultTolerance\Facades\FaultTolerance;
$response = FaultTolerance::for('inventory-service')
->retry(3, backoff: 200)
->timeout(2.0)
->circuitBreaker(threshold: 5, cooldown: 30)
->call(fn () => Http::get('https://inventory.internal/stock/42'));
if ($response->failed()) {
// degraded path
}
Adapt the fluent API to whatever the actual class exposes — the shape (named guard, retry, timeout, breaker, call) is the near-universal convention for this class of library. If the package instead centers on middleware for inbound requests, use that first: register the middleware on a single route group and watch logs for a day before widening it.
Per-downstream guard configuration. Define one named guard per external service in config/fault-tolerance.php, each with its own threshold, timeout, and cooldown. Sharing a single global breaker across all downstreams is the most common mistake — a slow email provider should not trip the breaker protecting your payment calls.
Retry only idempotent operations. Wrap GETs, PUTs, and DELETEs freely. For POSTs, require an idempotency key in the request before you allow retries. The library cannot know your semantics, so this is on you.
Fallback as a first-class path. Pair every guarded call with an explicit degraded response — cached value, stale data, queue-and-forget, or a user-visible "temporarily unavailable." A circuit breaker without a fallback just converts a timeout into a fast exception, which is better for latency but not for UX.
Bulkhead by module. If the package supports concurrency limits, cap how many in-flight calls each micro-module can make to each dependency. In a modular monolith with several "modules" talking to the same service, one chatty module can starve the rest.
Instrument from day one. Emit a metric or structured log line on every state transition (closed → open → half-open → closed) and on every retry attempt. Attach the guard name and the downstream host as labels. Without this you will discover a permanently-open breaker only when a user complains.
Layer with your HTTP client. Register the guard around Http:: calls, not inside a job's handle() body, so retries stay within a single request lifecycle and you don't accidentally retry an entire queued job.
Test the open state explicitly. Provide a way (test helper, fake clock, or a public forceOpen()) to put a guard into the open state and assert that the fallback path returns the degraded response. Also assert the half-open probe behavior — a breaker that never closes again is worse than no breaker.
Queues as the safety valve. For non-user-facing work, the most robust pattern is: try the guarded call, and on breaker-open, dispatch a job with a delay instead of failing. This turns "downstream is down" into "downstream is slow" from the user's perspective.
array resets on every request (useless); file doesn't work across multiple pods; use Redis or database. Check config/cache.php and the package's store config before trusting it in any horizontally-scaled deployment.retry(3) plus timeout(2.0) is a potential 6+ seconds of wall time, which will blow past PHP's max_execution_time or your load balancer's idle timeout. Budget total latency, not per-attempt latency.config:cache in production. If you publish config and later change breaker thresholds, php artisan config:cache in your deploy script will serve stale values until re-run. Make threshold changes part of a deploy, not a live edit.null silently hides outages. Log at warning with the guard name and the original exception before returning degraded data.~ constraint) in composer.json. Read the changelog between releases; with a package this young, minor bumps may include behavior changes to thresholds or state storage. Consider vendoring or forking if the surface area you use is small.RefreshDatabase-equivalent teardown or a package-provided reset helper in your base TestCase.CircuitOpened, CircuitClosed, RetryAttempted). The events are the cheapest way to hook in alerting without patching the package.How can I help you explore Laravel packages today?