The middleware above lets a request without a key through. That is the gentle way to introduce the feature: existing consumers keep working, and those who care can opt in.
For an endpoint where a duplicate costs real money, make the key required and answer 400 when it is missing. A consumer who is forced to send a key has been forced to think about retries. For the License API, where a duplicate license is an annoyance and not an invoice, optional is the right default.
Either way, the key itself is constrained, as the middleware does: a UUID or nothing.
The Case This Doesn’t Solve
Be honest with yourself about what the middleware guarantees. It guarantees that your API runs the work once per key. It can’t guarantee what happened at the provider.
Follow the worst case. The first attempt reaches Statamic, Statamic creates the license, and the connection to Statamic times out before the answer comes back. Your API returns a 504. Nothing was stored, correctly, because your API doesn’t know what happened. The consumer retries with the same key. The work runs again. Statamic creates a second license.
The key can’t fix this, because the uncertainty is one hop further out. There are two real answers.
Pass the key on. If the provider accepts an idempotency key of its own, send it the consumer’s. The guarantee then extends to the place where the license is actually created.
Look before you create. If it doesn’t, check what already exists. A license for acme.test that this consumer already holds is the answer to the request, not a conflict with it. With that, store has its shape. Chapters 18 and 19 each add one thing to it, and Chapter 19 prints it as it ends up:
// app/Http/Controllers/LicenseController.php
public function store(
StoreLicenseRequest $request,
): JsonResponse {
$consumer = $request->user();
$domain = $request->validated('domain');
$held = Licenses::all();
$license = $this->holding($held, $domain);
if ($license === null) {
if ($held->count() >= $consumer->license_limit) {
throw new LicenseLimitReached;
}
$license = Licenses::create(
name: $request->validated('name'),
domain: $domain,
);
}
return LicenseResource::make($license)
->response()
->setStatusCode(Response::HTTP_CREATED);
}
/** @param Collection<int, License> $held */
private function holding(
Collection $held,
string $domain,
): ?License {
foreach ($held as $license) {
if (in_array($domain, $license->domains, true)) {
return $license;
}
}
return null;
}
The list is fetched once and used twice: to look for the domain, and for Chapter 7’s limit. The order matters. A consumer at its limit that retries a create which already succeeded must be handed its license, not a 409, so the lookup comes before the limit.
The domain is a natural key: a value from the real world that already identifies the thing. A second attempt finds what the first one made and converges on it. Chapter 10 uses the same idea for jobs.
Whenever a real-world natural key exists, prefer it. An idempotency key protects against the retries a consumer chose to label. A natural key protects against all of them.