Todo lo anterior era recuperable.
Una respuesta mala se descarta y se reintenta. Una respuesta truncada se reporta. Un turno fallido escribe un error en una clave de caché y el usuario vuelve a intentarlo. Nada de lo que ha salido mal hasta ahora se ha quedado mal.
Este capítulo trata de la herramienta que crea algo real —un registro, una suscripción, un servidor aprovisionado, un recurso facturable— donde «inténtalo otra vez» no es una estrategia de recuperación, sino una forma de hacer el daño dos veces.
La instrucción no es la guarda
La herramienta que crea un sitio lleva una descripción clara. Verificación primero, un solo sitio, no repetir. El modelo la siguió fielmente.
Luego llegaron dos peticiones lo bastante seguidas como para que «una vez» fuera ambiguo, y una persona acabó con dos sitios.
No falló nada. La instrucción era correcta, se siguió cada vez y cada llamada era razonable por separado —porque una vez es una afirmación sobre el estado que ninguna de las dos llamadas podía observar. Dos llamantes sin una visión compartida del mundo no pueden imponer unicidad simplemente teniendo ambos la intención de hacerlo.
Una instrucción describe una intención. Una restricción la impone. Si lo único que impide un duplicado es una frase en una descripción, no tienes unicidad: tienes una preferencia fuerte.
Este es el cuarto principio del libro llegando donde cuesta dinero: el modelo es un llamante en el que no se confía, y nunca más que cuando está siendo complaciente.
Persiste primero, y que la base de datos sea la guarda
El arreglo no es una frase mejor. Es un índice único, y un orden que le permite hacer su trabajo:
// Persist the provisioning row first: its unique `url` index is the dedup
// guard that rejects a concurrent double-create with a friendly message
// instead of a raw 500 QueryException.
try {
$provisioning = ProvisioningSite::create([
'id' => $siteId,
'lead_id' => $leadId,
'url' => $fullUrl,
'email' => $validatedEmail,
'status' => 'provisioning',
'metadata' => $validated,
]);
} catch (QueryException) {
$this->restoreVerificationToken($verificationToken, $verificationPayload);
return Response::json([
'error' => 'A site is already being created for this address. Choose a
different URL, or wait for the current one to finish.',
]);
}
Tres decisiones en ese bloque.
La fila se escribe antes de que empiece el trabajo caro. Es una reclamación, no un registro de éxito. Quien la escriba primero se queda con la URL; el resto choca.
La colisión se captura y se traduce. Una QueryException sin capturar es un 500 que el modelo lee como un fallo inexplicado y que probablemente reintente. Capturarla produce algo sobre lo que el modelo puede actuar —elige otra URL— lo que convierte un error en una conversación.
El catch es tipado y sin cuerpo. catch (QueryException) sin ligar variable, porque la excepción no lleva nada que merezca mostrarse. La restricción es el mensaje.
Todo lo demás de este capítulo trata de las dos cosas que deben seguir siendo ciertas después de que se dispare ese catch.