Desarrollo de software a medida
Software a medida con Next.js y PostgreSQL
Qué cambia elegir PostgreSQL en una construcción con Next.js - las garantías que permite imponer, la búsqueda que ya no necesita un servicio, y la trampa en la que cae todo equipo una vez.
Elegir PostgreSQL para una construcción a medida tiene consecuencias concretas, y conviene decirlas en vez de darlas por supuestas. Es el valor por defecto en estos proyectos, no una regla, y el motivo es que mueve trabajo fuera de la aplicación hacia un sitio donde no se puede sortear.
Garantías que la aplicación no puede olvidar
El argumento más fuerte a favor de una base de datos relacional en software a medida no tiene nada que ver con el rendimiento.
Una aplicación la escriben varias personas a lo largo de varios años, y no todas conocerán las reglas. Una garantía impuesta en el código de la aplicación se cumple en los caminos que alguien recordó, y no en el script de importación, el comando de consola, la tarea programada ni la pantalla de administración que se añade la primavera que viene.
Una garantía impuesta en la base de datos se cumple en todos. Una clave ajena significa que un pedido no puede referenciar un cliente que no existe. Una restricción de unicidad significa que dos peticiones simultáneas no pueden crear las dos el mismo registro, algo que no consigue ninguna cantidad de comprobar-y-luego-insertar en el código. Una restricción de comprobación significa que una cantidad no puede ser negativa en la fila, no solo en el formulario.
PostgreSQL va más allá que la mayoría de una manera que importa en aplicaciones reales. Una restricción de exclusión puede rechazar una reserva cuyo rango de fechas se solapa con otra existente del mismo recurso: una condición de carrera que es verdaderamente difícil de cerrar en el código y trivial de cerrar aquí. Los índices parciales permiten indexar solo las filas que importan, para que una consulta sobre registros activos no pague por el archivo.
Qué elimina eso del código
Cada una de esas restricciones es código de validación que no escribes, no pruebas y no tienes que recordar cuando añades una séptima forma de crear un registro.
Es además la diferencia entre un fallo imposible y un fallo improbable. En un sistema que maneja dinero, stock o citas, esa distinción es todo el asunto.
JSONB, y dónde está la línea
Algunos datos se resisten de verdad a un esquema. La carga de un webhook de un tercero. Un formulario cuyos campos configura el cliente. Ajustes por inquilino que difieren según el inquilino.
PostgreSQL lo trata con honestidad: JSONB se almacena de forma eficiente, se consulta con operadores reales y se indexa con GIN, así que los datos sin estructura son una opción de primera clase y no un apaño.
La línea que conviene mantener es que todo aquello por lo que filtras, ordenas, informas, unes o restringes tiene una columna. El patrón de fallo es conocido y lento: una columna JSON se convierte en el sitio al que van los campos nuevos porque añadir una migración parecía pesado, y dos años después nadie puede responder una pregunta sobre los datos sin leer código.
Búsqueda, sin un segundo sistema
Una buena parte de las aplicaciones añade un servicio de búsqueda, una tubería de sincronización y una dependencia operativa para hacer algo que la base de datos ya sabe hacer.
La búsqueda de texto completo en PostgreSQL tiene lematización, ranking y configuraciones para varios idiomas, y se indexa bien. Con unos cientos de miles de registros es rápida, y tiene la ventaja considerable de que los resultados no pueden quedarse obsoletos, porque no hay nada que sincronizar.
Donde se agota es real y merece nombrarse: tolerancia a erratas, ajuste de relevancia a gran escala, búsqueda facetada sobre millones de documentos. Llegar a ese punto es una medición, no una predicción, y llegar allí más tarde cuesta mucho menos que operar un clúster de búsqueda desde la primera semana para un conjunto de datos que nunca lo necesitó.
La única trampa, y cómo se trata
Todo equipo se la encuentra una vez. Una aplicación Next.js en un entorno serverless abre una conexión a la base de datos por instancia, y PostgreSQL acepta un número fijo. Con tráfico la aplicación no se vuelve más lenta: empieza a rechazar conexiones, lo que se lee como un fallo de la base de datos y es un error de configuración.
La respuesta es un pooler de conexiones entre la aplicación y la base de datos, y la parte que pilla a la gente es que el pooling en modo transacción es incompatible con las sentencias preparadas salvo que el cliente esté configurado para ello. El pooling de conexiones en un entorno serverless es la explicación completa.
En estas construcciones se decide en la semana uno, y esa es la diferencia entre una restricción conocida y un incidente.
Cómo se desarrolla el proyecto
Por fases, cada una fijada por escrito antes de empezar y cada una terminando con algo desplegado que puedes usar. La primera fase coge un flujo completo de principio a fin en vez del contorno de varios, porque eso es lo que decide el esquema contra datos reales.
Las decisiones de base de datos se toman en esa primera fase y no después. Qué restricciones se imponen y dónde, qué es una columna y qué es JSONB, cómo corren las migraciones contra una tabla que después será grande, y cuál es el arreglo de pooling. Ninguna de ellas es caro decidirla pronto y todas son caras de cambiar cuando ya hay datos de producción detrás.
Qué recibes
Una aplicación desplegada al final de cada fase, el repositorio con su historial intacto, y el esquema como migraciones en vez de como estado actual, para que la base de datos tenga un historial que se pueda reproducir y revisar.
Con ello: una nota escrita sobre el modelo de datos y por qué tiene esa forma, los índices y para qué sirve cada uno, la configuración del pooling, y las restricciones con una línea cada una explicando qué evitan. Ese último es el documento al que la gente vuelve de verdad, porque una restricción que nadie entiende es una restricción que alguien acaba quitando.
Cuándo PostgreSQL no es la respuesta
Si la aplicación es una capa fina sobre la API de otro y casi no guarda estado propio, estás operando un componente sin motivo, y un almacén más simple es la mejor respuesta.
Si la carga es realmente una caché o una cola, debería ser una caché o una cola. PostgreSQL hace las dos cosas de forma aceptable y no está pensada para ninguna.
Y si los datos son mediciones de alto volumen que solo se añaden - lecturas de sensores, telemetría a escala -, una tabla relacional aguantará una cantidad sorprendente, pero hay un punto más allá del cual gana un almacén diseñado para eso. Ese punto debería establecerse midiendo y no suponiendo que ya se ha alcanzado.
