What you'll learn
Improve response time without trading away correctness. Cache derived reads with an invalidation policy and move slow work to retryable, observable jobs designed for duplicate delivery.
By the end of this lesson, you'll be able to:
- Implement cache-aside safely
- Design an idempotent background job
- Choose retry, dead-letter, scheduling, and observability policies
Core mental model
Professional PHP is less about memorizing APIs and more about choosing a clear boundary for each responsibility. Use this table as a decision guide while reading the examples.
| Concept | What it protects | Decision rule |
|---|---|---|
| Cache-aside | Faster repeated reads | Cache derived data with TTL and invalidation ownership. |
| At-least-once delivery | Durable queues | Assume a job may run more than once. |
| Idempotency key | Duplicate-effect defense | Persist completion under a stable operation identity. |
Professional workflow
Build the feature in small, verifiable steps. Each step leaves the system in a state you can test.
- Describe the asynchronous workload boundary: its inputs, outputs, invariants, and expected failures.
- Implement the smallest happy path behind an explicit contract.
- Add validation and translate low-level failures into language the caller understands.
- Exercise the boundary with realistic data, then inspect output, logs, and resource cleanup.
- Refactor only after behavior is protected by a repeatable check.
Make the boundary visible
Guided code lab
Use cache-aside
The repository remains authoritative; the cache accelerates a named view with a bounded lifetime.
<?php
final readonly class CachedCatalog
{
public function __construct(private Cache $cache, private CourseRepository $courses) {}
public function featured(): array
{
return $this->cache->remember(
'catalog:featured:v2',
ttlSeconds: 300,
compute: fn (): array => $this->courses->featured(),
);
}
public function invalidateFeatured(): void { $this->cache->delete('catalog:featured:v2'); }
}Make a job duplicate-safe
A unique operation key turns redelivery into a no-op after success.
<?php
final readonly class SendWelcomeEmail
{
public function __construct(private JobLedger $ledger, private Mailer $mailer) {}
public function handle(int $userId): void
{
$key = "welcome-email:$userId";
if ($this->ledger->completed($key)) return;
$this->mailer->sendWelcome($userId);
$this->ledger->markCompleted($key);
}
}Production practice
Contract
Jobs carry identifiers, not serialized domain graphs; handlers reload current state and record outcomes.
Verification
Run handlers twice, force transient failures, expire caches, and assert retries do not duplicate external effects.
Operations
Measure queue depth, oldest-job age, attempts, failures, dead letters, cache hit ratio, and evictions.
Common failure mode
Independent workshop
Add cached dashboards and queued report emails to the content application while preserving correctness under redelivery.
Your finished workshop must include:
- Versioned cache keys and invalidation plan
- Idempotent job plus retry policy
- Worker metrics and dead-letter recovery runbook
Definition of done
Recap & quick check
Key takeaways
- The database remains the source of truth.
- Cache invalidation is part of feature design.
- Queue delivery may repeat.
- Async systems require observable lifecycle states.
Quick check
1. What must a queue consumer assume?
2. What does a cache TTL provide?
3. Which metric reveals growing delay?
Keep the workshop: later phases deliberately build on these boundaries, so today's small example can become part of your portfolio architecture.