Inicio / Artículos / Siete preguntas que resolver antes de escribir la primera línea de una crónica

Siete preguntas que resolver antes de escribir la primera línea de una crónica

Una lista de verificación previa a la implementación que abarca el problema real del usuario, las garantías de finalización, la responsabilidad por las reglas, los datos heredados, las reintentos y la concurrencia, la observabilidad y la seguridad en el despliegue.

2797 palabras

La forma más rápida de sentirse productivo con un nuevo ticket es abrir un editor y comenzar a desarrollar: agregar el punto de extremo, la columna y el componente. El problema es que la primera implementación responde silenciosamente a todas las preguntas que nadie hizo, y esas respuestas se convierten en esquemas, contratos de API y pruebas cuya modificación resulta costosa. Esta guía explica siete preguntas que los ingenieros experimentados resuelven antes de codificar, qué sucede cuando se omiten y cómo mantener el proceso en proporciones adecuadas para acelerar la entrega en lugar de ralentizarla.

Por qué la primera implementación tiene tanta importancia

Cuando llega una solicitud, comenzar con el código hace que una tarea abstracta parezca concreta y más sencilla. Sin embargo, las preguntas pendientes no desaparecen. Alguien todavía tiene que decidir quién es el responsable de una regla de negocio, qué significa “terminado” en una operación de varios pasos y qué ocurre con los registros creados antes del cambio. Si nadie toma una decisión, el código lo hace por casualidad.

Esa decisión accidental rara vez se limita a un solo lugar. La estructura de la primera versión tiende a convertirse en el diseño de la tabla, el formato de respuesta, la función auxiliar compartida que todos importan y el modelo de estado del que depende la interfaz de usuario. Una vez que otro código la utiliza y los datos en producción se ajustan a ella, cambiar de rumbo implica migraciones, soluciones de compatibilidad y lanzamientos coordinados. Pasar una hora respondiendo las preguntas adecuadas desde el principio resulta económico en comparación.

Desde afuera, esto puede parecer vacilación: leer los flujos de trabajo existentes, preguntarse qué es lo que el usuario realmente intenta lograr, verificar cómo se comportan los datos antiguos, discutir fallos parciales. En la práctica se trata del mismo proceso de resolución de problemas que el código tendría que realizar de todos modos, solo que se hace mientras aún es económico cambiar de opinión.

1. Separe la función solicitada del problema subyacente

A menudo las solicitudes llegan como soluciones: agregar un botón, un filtro, una opción de exportación, un nuevo estado. La petición puede ser completamente razonable, pero describe lo que alguien imagina que se está creando, no la frustración o el resultado empresarial detrás de ella.

Al abordar esa necesidad subyacente, cambia lo que se crea. Una solicitud de exportación en formato CSV podría provenir realmente de gerentes que no pueden comparar las cifras semanales entre departamentos. La exportación ayuda, pero también genera trabajo manual recurrente con hojas de cálculo, algo que un informe guardado o un resumen programado podrían eliminar por completo. Una solicitud de agregar otro valor de estado podría revelar que un único campo ya está sobrecargado, al tener que representar al mismo tiempo el pago, la aprobación y el cumplimiento. Al añadir ese valor se cierra el caso, pero el modelo de datos se vuelve aún más difícil de comprender.

Nada de esto implica interrogar cada pequeña solicitud o convertir un cambio sencillo en una reunión de desarrollo de producto. El objetivo es conocer lo suficiente sobre la situación del usuario como para determinar si el cambio propuesto realmente mejora el resultado. Por lo general, bastan unas pocas preguntas directas:

  • ¿Quién no puede terminar su trabajo hoy?
  • ¿Qué están haciendo actualmente a mano?
  • ¿Qué decisión respaldará esta nueva información?
  • ¿Qué se vuelve posible una vez que existe la función?
  • Si se omite este paso, los ingenieros optimizarán naturalmente la solicitud tal como está formulada. El botón es limpio, el componente reutilizable, la API ordenada, pero la función sigue decepcionando porque resuelve el ticket con mayor precisión de lo que realmente soluciona el problema. El objetivo no es retrasar la codificación, sino asegurarse de que el código sea la respuesta correcta.

    2. Definir qué significa el éxito para toda la operación

    Muchos requisitos mencionan una acción sin definir cuándo se considera completada. Presentar una solicitud, aprobar un registro, duplicar un proyecto o sincronizar datos suenan sencillos hasta que intervienen varios pasos y uno de ellos falla.

    Considere un flujo de aprobación que persista el cambio, escriba una fila en el registro de auditoría, emita un evento y notifique al solicitante. Si la escritura tiene éxito pero la notificación falla, ¿se ha completado con éxito la aprobación? Si el usuario vuelve a intentarlo, ¿podría el registro ser aprobado dos veces? Si no se puede escribir la entrada de auditoría, ¿debería anularse el cambio de estado? Si el evento se retrasa, ¿está la aprobación finalizada o pendiente? Estos no son detalles menores de implementación; definen lo que el producto promete a sus usuarios.

    El ejercicio útil es trazar los límites de finalización antes de diseñar el flujo de trabajo:

    • Los efectos que deben tener éxito o fallar juntos, ya que un resultado parcial constituiría un estado inválido, deben formar parte de una sola transacción.
  • Los efectos que son valiosos pero secundarios, como un correo electrónico de bienvenida, no deben determinar si la acción principal tuvo éxito. Con frecuencia, deberían formar parte de una tarea en segundo plano con intentos repetidos y un registro explícito de “pendiente”.
  • La misma pregunta surge en las funcionalidades más simples. Cuando alguien inicia una exportación, ¿se considera exitosa una vez que existe el archivo y la tarea fue aceptada, o aparecerá un enlace más tarde? Cuando la interfaz muestra “Guardado”, ¿el servidor confirmó que la información se conservó o solo cambió el estado local?

    Dejarlo vago hace que cada capa invente su propia definición. La interfaz de usuario muestra éxito mientras el backend sigue procesando, un proceso reintentado algo que el usuario ya considera fallido, y la supervisión informa de una solicitud correcta aunque un efecto secundario esencial haya desaparecido. Establecer primero las garantías tiende a simplificar la implementación, ya que cada paso ahora tiene una función clara. También da forma al contrato de respuesta: una API que devuelve “accepted” es diferente de una que devuelve “done”, y los clientes necesitan saber cuál están recibiendo.

    3. Decidir qué capa se encarga de cada decisión

    Una función puede producir resultados correctos y seguir siendo insegura si la decisión se toma en la capa equivocada. Ejemplos comunes:

    • El frontend oculta un botón de los usuarios sin permiso, pero la API acepta la solicitud cuando se realiza directamente.
  • Un controlador valida una transición de estado, pero un trabajo programado llama al servicio subyacente y omite esa verificación.
  • Un servicio rechaza los duplicados, pero un script de mantenimiento escribe directamente en la tabla.
  • En cada caso existe la regla, pero no todos los flujos pasan por ella obligatoriamente.

    La solución consiste en encontrar la capa con suficiente autoridad para gestionar dicha regla. La interfaz de usuario puede reflejar los permisos para facilitar el uso, pero nunca constituye el límite de seguridad. Un controlador es un lugar adecuado para validar la estructura de una solicitud HTTP, mientras que las reglas de negocio suelen necesitar aplicarse en capas más profundas para que los trabajos en segundo plano y las llamadas internas tengan un comportamiento idéntico. Una verificación de unicidad a nivel de aplicación genera errores amigables, pero solo una restricción en la base de datos protege realmente la invariante cuando se realizan escrituras simultáneas.

    La propiedad se aplica tanto al estado como a las reglas. Los filtros que deben ser compartibles y seguir funcionando tras una actualización son adecuados para el URL. La entrada temporal pertenece al formulario. Los permisos y el estado persistente provienen del servidor, y el cliente no debe reconstruir su propia versión a partir de suposiciones locales.

    Cuando la autoridad no está clara, se genera un código de coordinación: verificaciones duplicadas en varios niveles, copias del mismo valor que deben mantenerse sincronizadas, y cambios que se convierten en búsquedas para reemplazarlos. Una actualización de políticas afecta entonces la interfaz de usuario, el controlador, el servicio, un proceso y una o dos consultas, sin garantía de que cada copia siga significando lo mismo. Asigne a cada decisión importante un responsable designado. Otros niveles pueden mostrar, almacenar en caché, aplicar o transmitir el resultado, pero no deben redefinirlo. Esto reduce tanto el riesgo de seguridad como los costos de mantenimiento, ya que todos saben dónde se encuentra la fuente de verdad.

    4. Tener en cuenta los datos que ya existen

    Se escribe nuevo código para el modelo que se desea utilizar. Sin embargo, los datos de producción también contienen rastros de todos los modelos anteriores.

    Hacer que un campo sea obligatorio es sencillo para los registros creados después del lanzamiento, pero miles de filas antiguas podrían no tenerlo. Un modelo de estado rediseñado puede describir bien los flujos de trabajo futuros, pero dejará a las filas históricas atrapadas en estados que el nuevo código ya no reconoce. Una relación recién hecha obligatoria puede apuntar a una entidad que simplemente no existía cuando se crearon las filas antiguas.

    Antes de agregar validaciones o cambiar el esquema, pregúntese:

    • ¿Se pueden migrar los registros existentes de manera fiel?
    • ¿Se necesita un estado temporal de “desconocido”?
    • ¿Este cambio está reescribiendo la historia o solo modificando el comportamiento futuro?

    La palabra importante es, sin duda, “verdad”. Rellenar cada vacío con un valor predeterminado conveniente puede satisfacer la restricción NOT NULL al mismo tiempo que se introducen datos falsos. Si el departamento responsable de un registro antiguo nunca fue registrado, asignarle el departamento actual simplifica las consultas pero hace que los informes históricos sean menos fiables. A veces, un esquema honesto permite incluir valores como “desconocido” o “heredado”, ya que no saberlo forma parte real del pasado de ese registro.

    También existen formatos antiguos fuera de la base de datos. Los clientes más antiguos pueden seguir enviando formatos de carga previos, los trabajos programados pueden depender de valores de estado que el nuevo flujo desea eliminar, y los informes pueden interpretar las columnas según reglas que cambiaron hace tiempo.

    No es necesario seguir apoyando todo comportamiento del pasado indefinidamente, pero la decisión de migrar debe ser explícita. Algunos datos pueden transformarse de forma segura, otros requieren revisión manual, algunos clientes antiguos merecen un período de compatibilidad y hay otros que pueden eliminarse intencionadamente. Ignorar el problema no hace que desaparezca; vuelve a surgir en forma de soluciones temporales dispersas, columnas nulas que nadie entiende, migraciones fallidas e incidentes de soporte después del despliegue. Decidir con antelación permite al equipo realizar una transición coherente en lugar de depender de numerosas conjeturas locales.

    5. Suponga que el trabajo se repetirá y competirá entre sí

    Las descripciones de funcionalidades suelen imaginar un único usuario, un solo clic y una secuencia limpia: la solicitud llega una vez, nada más afecta al registro mientras tanto y la respuesta llega al cliente. La producción no ofrece ninguna de esas garantías.

    • Un usuario vuelve a hacer clic porque la página parece estar atascada.
  • La conexión móvil se interrumpe después de que el servidor termine su trabajo, pero antes de que llegue la respuesta.
  • Una cola entrega el mismo mensaje dos veces.
  • Dos administradores aprueban el mismo elemento pendiente con pocos segundos de diferencia.
  • Un trabajo programado actualiza un registro que el usuario aún está viendo en una versión anterior.
  • La pregunta que hay que plantearse de antemano es si es seguro ejecutar la operación más de una vez y si es seguro hacerlo de forma concurrente. Si la repetición no causa daños, puede que no sea necesario utilizar mecanismos adicionales. Si la repetición genera un segundo pago, invitación, archivo o reserva de inventario, el sistema necesita una forma de reconocer que varios intentos representan una sola acción lógica.

    La caja de herramientas incluye claves de idempotencia, restricciones únicas, actualizaciones condicionales, columnas de versión para el bloqueo optimista, transacciones y una tabla con los IDs de los mensajes procesados. La opción adecuada depende de dónde resida el riesgo. Lo que nunca funciona es decir “primero lo verificamos”, como si no pudiera haber otro proceso que actúe entre la verificación y la escritura.

    Los errores de concurrencia son especialmente peligrosos porque cada paso parece correcto al revisarlo. El defecto se encuentra en el intervalo entre la lectura y la escritura: dos solicitudes leen una captura idéntica y válida, ambas pasan sus verificaciones y cada una registra un resultado que debería haber ocurrido solo una vez. Es mucho mejor que la base de datos rechace una de las dos operaciones concurrentes mostrando un conflicto visible, en lugar de almacenar dos verdades contradictorias que alguien tendrá que resolver manualmente más tarde.

    6. Planificar cómo la función se explicará en producción

    En su máquina cuenta con puntos de interrupción, la posibilidad de repetir una acción y un estado actualizado del diseño. En producción, el equipo puede recibir únicamente un mensaje de soporte indicando que algo no funcionó.

    Imagínese, entonces, la investigación que es necesario realizar antes de escribir el código. Si la operación falla, ¿cómo se sabrá qué paso fue el problema? ¿Se puede rastrear una sola solicitud a través de varios servicios? ¿Los registros mostrarán si la acción se ejecutó una vez o tres veces? ¿Es posible distinguir entre “nunca iniciado”, “en curso”, “parcialmente completado” y “fallido”?

    Esto no significa que sea necesario registrar todo. Una gran cantidad de datos desestructurados dificulta las investigaciones, en lugar de facilitarlas. Una observabilidad útil captura solo lo suficiente como para reconstruir el historial de una operación importante:

    • Un ID de solicitud o de correlación
    • El ID del recurso y el nombre de la operación
    • La duración
  • La transición de estado que ocurrió
  • Número de intentos
  • Categoría de error estable
  • También hay que preservar el significado del fallo. Una consulta fallida no debería convertirse silenciosamente en una lista vacía. Un tiempo de espera del proveedor no debería ignorar el hecho de que el lado remoto podría haber completado la tarea. Un bloque de captura general no debería reducir cada causa a un mensaje genérico antes de que llegue al sistema de registro.

    Piense también en la recuperación. ¿Es seguro volver a ejecutar una tarea que falló? ¿Puede el soporte técnico ver el estado actual de una operación sin consultar varias tablas manualmente? ¿Puede el usuario intentarlo nuevamente de forma segura, y alguien puede darle una descripción precisa del resultado?

    Las funcionalidades opacas se vuelven costosas en cuanto ocurre un problema, y agregar registro posteriormente suele ser demasiado tarde, ya que el contexto relevante solo existió mientras se ejecutaba la operación. Diseñar las pruebas desde el principio hace que formen parte de la funcionalidad en sí y no solo un parche de emergencia después de un incidente.

    7. Decide cómo demostrarás que el cambio es seguro

    Cuando las pruebas se escriben después del código, tienden a reflejar su estructura actual. Existe una función auxiliar, por lo que la prueba verifica que se llamó; existe un mecanismo de respaldo, por lo que la prueba asegura que se utilice; se simula un repositorio, por lo que la prueba confirma que el simulacro devolvió lo que se le indicó. Dichas pruebas pasan sin demostrar mucho realmente.

    Decidir primero sobre la prueba suele revelar debilidades en el diseño. Si una operación debe ser idempotente, la prueba debería ejecutarla varias veces. Si las aprobaciones concurrentes de un registro deben ser imposibles, la prueba necesita actualizaciones realmente concurrentes. Si “no encontrado” y “fallido” son resultados diferentes, el contrato debe hacer que ambos sean observables. Si la regla se aplica mediante una restricción de base de datos, ninguna prueba unitaria simulada puede demostrar que se cumple.

    Eso no significa que cada funcionalidad necesite un conjunto completo de pruebas end-to-end. Elija el nivel de prueba según la capa que realmente aplique la garantía:

    • Una transformación pura puede probarse directamente como prueba unitaria.
    • Un contrato de API suele requerir una prueba de integración.
    • Una invariante aplicada en la base de datos debe probarse contra una real.
  • Una migración arriesgada podría requerir monitoreo, un despliegue por fases o una marca de funcionalidad con una fecha planificada para su eliminación.
  • La reversibilidad también debe considerarse en la misma conversación. Si el cambio funciona mal, ¿se puede desactivar o revertir sin perder los datos escritos hasta ese momento? ¿Seguirá funcionando la versión anterior de la aplicación después del cambio de esquema, o la migración necesita una secuencia de expansión y contracción? ¿Es posible lanzarla primero en un grupo pequeño antes de que todos dependan de ella?

    Si un diseño es difícil de probar o de revertir, eso suele ser una señal de que una operación asume demasiadas responsabilidades o de que el cambio necesita un paso intermedio más sencillo. La prueba absoluta no es el objetivo, ya que el software siempre conlleva incertidumbre. Lo que se busca es que las garantías críticas sean visibles en las pruebas y métricas, y que las decisiones peligrosas puedan anularse, de modo que un error se convierta en una lección en lugar de un daño permanente.

    Mantener el trabajo previo en proporción

    Estas preguntas no constituyen un argumento en favor de que la planificación siempre sea mejor que la acción directa. Un análisis excesivo puede retrasar trabajos sencillos y generar arquitecturas para riesgos que nunca se materializarán. Un filtro razonable consiste en centrarse únicamente en las decisiones que resultarían costosas si se cometen errores en el código: todo aquello relacionado con datos persistentes, dinero, permisos, efectos secundarios externos o contratos públicos. Un cambio de copia o una refactorización interna detrás de una interfaz estable rara vez requiere toda la lista.

    En su forma más sencilla, la lista de verificación cabe en la descripción de un ticket:

    • El problema real, en una sola oración, y quién lo tiene
    • Qué significa “hecho” y qué efectos son secundarios
    • El responsable único de cada regla de negocio
    • El plan para los datos existentes y los clientes antiguos
    • El comportamiento al intentar nuevamente y bajo acceso concurrente
    • Qué se registra y cómo se recuperan los fallos
  • Cómo se probará la garantía y cómo se podrá revertir el cambio
  • Conclusión

    Una vez que se tienen respuestas para estas preguntas, el código suele volverse notablemente directo. El modelo de estado tiene menos combinaciones imposibles, cada regla tiene un lugar definido, la base de datos aplica las invariantes, las respuestas indican si el trabajo se completó o simplemente fue aceptado, y las pruebas se centran en la garantía en lugar de en la disposición actual de las funciones.

    Por eso, los ingenieros cuidadosos pueden parecer lentos al inicio de una tarea y aún así terminarla antes: se niegan a permitir que la ambigüedad del producto, los datos heredados, la concurrencia y los puntos ciegos operativos se conviertan silenciosamente en decisiones técnicas permanentes. El momento adecuado para abordar esos problemas es antes de que la primera implementación conveniente afecte a las llamadas, las pruebas y los datos de producción. Escribir la función rara vez es la parte difícil; lo complicado es decidir qué significa.

    Lecturas relacionadas

  • Diseño de Sistema Backend por Cuello de Botella: De Acortador de URL a Comercio Electrónico — Un enfoque centrado en los requisitos para diseñar backends en Node.js: cuándo añadir balanceadores de carga, Redis, réplicas, colas y límites de tasa, y cuánto le cuesta cada uno.