Si construyes sistemas distribuidos, ya conoces el problema: el estado se te escapa entre servicios, colas, retries, timeouts y jobs que quedan a medio camino. Un flujo que parecía simple termina repartido entre Redis, una cola, una tabla de auditoría, un worker y un servicio que “promete” terminar más tarde. Cuando algo falla, no siempre sabes si el pedido quedó cobrado, si el correo salió, si el inventario se reservó o si el paso siguiente siquiera se intentó.
La idea central de este artículo es simple: puedes usar las transacciones de Postgres como base de consistencia para workflows distribuidos. No para reemplazar todo, sino para reducir el número de estados intermedios frágiles y concentrar la verdad del sistema en el mismo lugar donde ya guardas tus datos. Eso baja complejidad, mejora la observabilidad y te da una forma más predecible de coordinar pasos que, de otro modo, vivirían dispersos.
El problema real: demasiados estados, demasiados lugares
Cuando un workflow crece, suele dividirse en piezas pequeñas por razones válidas: desacoplamiento, escalabilidad, resiliencia. El problema aparece cuando cada pieza empieza a guardar una parte distinta de la verdad. Tienes el estado del pedido en una tabla, la cola de tareas en otro lado, el progreso del job en memoria o en Redis, y los eventos de integración en un bus que quizá no es duradero para tu caso de uso.
Ese reparto parece elegante al inicio, pero introduce un costo muy concreto: cada transición necesita coordinación. Si cobras antes de reservar inventario, puedes dejar un pedido pagado sin stock. Si reservas antes de confirmar el pago, puedes bloquear inventario para órdenes que nunca se completan. Si mandas un email antes de persistir el cambio, puedes notificar algo que luego se revierte.
El patrón que suele aparecer es el de estados intermedios frágiles. Por ejemplo: pending_payment, payment_processing, inventory_reserved, email_queued, shipment_requested. Cuantos más estados agregas para “controlar” el flujo, más oportunidades tienes de quedar atrapado entre dos pasos. Y cuando eso pasa, el equipo termina escribiendo scripts manuales, reintentos ad hoc o paneles internos para reparar inconsistencias.
Qué cambia cuando la base es la transacción
Una transacción de Postgres te da una propiedad muy útil: o se guardan todos los cambios, o no se guarda ninguno. En un workflow, eso significa que puedes registrar el avance del proceso y los efectos de negocio de forma atómica, evitando que un paso quede visible sin el otro. No resuelve todo lo distribuido, pero sí elimina una gran clase de problemas locales que luego se vuelven distribuidos por accidente.
La idea no es “meter todo en una sola tabla”. La idea es usar Postgres como ancla de consistencia para el estado del workflow. El resto del sistema puede seguir siendo distribuido, pero el punto donde decides “esto ocurrió” vive en una transacción que también deja trazas duraderas y consultables.
Eso encaja especialmente bien cuando tu aplicación ya depende de Postgres para el core del negocio. Si ya tienes ahí pedidos, usuarios, facturas o inventario, agregar el estado del workflow en la misma base suele ser más barato que introducir otra pieza de infraestructura solo para coordinar pasos.
Cómo se ve un workflow co-localizado con tus datos
La forma más práctica de pensar esto es: el workflow no vive “al lado” de tus datos, vive con tus datos. En lugar de tener un orquestador separado que almacena su propio estado en otro sistema, escribes el progreso del workflow en la misma base donde guardas la entidad principal. Así, el estado del proceso y el estado del negocio evolucionan juntos.
Un ejemplo típico es un checkout. Imagina que el usuario confirma una compra. En una sola transacción puedes:
- crear el pedido con estado
confirmed, - reservar stock en la tabla de inventario,
- registrar un evento interno para procesar el pago,
- guardar el paso actual del workflow,
- dejar listo un registro para el worker que enviará el correo o disparará la logística.
Si algo falla en el paso 3 o 4, haces rollback y no queda un pedido “a medias”. Si la transacción committea, sabes que el sistema tiene una base consistente desde la cual seguir.
Un ejemplo mínimo en SQL
No necesitas un framework exótico para entender la idea. Un flujo simple podría empezar así:
BEGIN;
UPDATE inventory
SET available = available - 1
WHERE sku = 'SKU-123' AND available > 0;
INSERT INTO orders (id, user_id, status, created_at)
VALUES ('ord_1001', 'usr_42', 'confirmed', NOW());
INSERT INTO workflow_state (workflow_id, order_id, step, updated_at)
VALUES ('wf_7788', 'ord_1001', 'order_created', NOW());
COMMIT;
Si el UPDATE no afecta filas porque no hay stock, puedes abortar antes de crear el pedido. Si el INSERT del workflow falla, no dejas un pedido confirmado sin trazabilidad. La clave es que el sistema no depende de que otro servicio “alcance” a persistir algo después.
Esto no significa que todo el trabajo pesado ocurra dentro de la transacción. Significa que el punto crítico de verdad del negocio sí queda protegido. Lo demás puede seguir en background, con workers y reintentos, pero arrancando desde un estado ya consistente.
Patrones útiles para no romper la consistencia
Hay varias formas de aprovechar Postgres sin convertirlo en un cuello de botella. La más conocida es el outbox pattern, donde escribes en la misma transacción el cambio de negocio y un registro de evento para procesar después. Otra opción es usar una tabla de jobs o de workflow state que los workers consumen con locking controlado.
La ventaja es que reduces la dependencia de mensajes “fantasma” que podrían perderse entre el commit y el publish. Según la documentación oficial de Postgres, SKIP LOCKED permite que múltiples workers tomen trabajo sin bloquearse entre sí, algo muy útil para colas basadas en tabla. Puedes revisar la referencia oficial aquí: https://www.postgresql.org/docs/current/sql-select.html
Outbox, jobs y estado del workflow
Estos tres enfoques no compiten tanto como parecen. En muchos sistemas terminan combinados:
- Outbox: guardas un evento confiable en la misma transacción del negocio.
- Jobs table: registras tareas pendientes para que workers las procesen.
- Workflow state: guardas el paso actual y sus metadatos para reanudar o auditar.
La diferencia principal está en el objetivo. El outbox sirve para publicar hacia afuera sin perder eventos. La tabla de jobs sirve para ejecutar trabajo asíncrono. El workflow state sirve para saber en qué punto está el proceso y decidir el siguiente paso.
En una arquitectura sencilla, puedes usar solo una tabla de workflow y una de outbox. En una más madura, separas responsabilidades, pero mantienes la misma regla: todo lo que define la verdad del negocio se escribe dentro de una transacción.
Cuándo usar locking explícito
No todos los workflows necesitan locks pesados, pero algunos sí. Si dos workers pueden intentar procesar el mismo pedido, necesitas exclusión mutua. Ahí entran SELECT ... FOR UPDATE, NOWAIT o SKIP LOCKED, según el caso.
Ejemplo práctico: si tu worker busca órdenes pendientes, puede tomar una fila, marcarla como processing y seguir. Si otro worker intenta tomarla al mismo tiempo, SKIP LOCKED evita que ambos hagan el mismo trabajo. Eso reduce duplicados sin obligarte a inventar una coordinación externa más compleja.
Un patrón frecuente es este:
BEGIN;
WITH next_job AS (
SELECT id
FROM workflow_jobs
WHERE status = 'pending'
ORDER BY created_at
FOR UPDATE SKIP LOCKED
LIMIT 1
)
UPDATE workflow_jobs
SET status = 'processing', started_at = NOW()
WHERE id IN (SELECT id FROM next_job)
RETURNING *;
COMMIT;
Con eso, el worker obtiene una tarea de forma segura. Si no hay filas disponibles, simplemente no toma nada y puede reintentar más tarde.
Qué ganas en arquitectura y operación
La primera ganancia es menos piezas. Si el estado vive en Postgres, ya no necesitas otro almacén para la coordinación básica del workflow. Eso reduce configuración, despliegues, backups, permisos, monitoreo y puntos de falla. Para equipos chicos o medianos, ese ahorro se nota rápido.
La segunda ganancia es observabilidad. Puedes consultar el estado del workflow con SQL, unirlo con pedidos, usuarios o facturas, y responder preguntas operativas sin cruzar tres sistemas. Por ejemplo: cuántos pedidos están en processing, cuántos llevan más de 10 minutos sin avanzar, qué cliente tiene más reintentos, o en qué paso falla más seguido el flujo.
La tercera ganancia es que los retries dejan de ser una ruleta. Si una tarea se cae después del commit, el sistema sabe qué quedó persistido y qué falta. No tienes que inferir el estado desde logs o desde una cola que quizá ya no conserva suficiente contexto.
Costos y límites que sí debes tener en cuenta
Esto no es magia y tampoco conviene usarlo para todo. Si tu workflow dispara miles de eventos por segundo o requiere coordinación entre varias bases de datos, Postgres puede seguir siendo parte de la solución, pero no necesariamente el único componente. También debes cuidar la duración de las transacciones: mantener locks por demasiado tiempo afecta concurrencia.
Hay otra regla práctica: no metas trabajo externo dentro de la transacción. Llamar a una API, enviar un email o esperar una respuesta de terceros mientras mantienes la transacción abierta es mala idea. Lo correcto es persistir el estado, commit, y luego ejecutar el side effect desde un worker o proceso separado.
Si necesitas una referencia para entender mejor el comportamiento de transacciones y aislamiento, la documentación oficial de Postgres es un buen punto de partida: https://www.postgresql.org/docs/current/tutorial-transactions.html
Un diseño práctico para equipos en producción
Si vas a aplicar este enfoque, conviene empezar con una estructura simple. No necesitas rediseñar toda tu plataforma. Basta con identificar un flujo crítico, normalmente uno que hoy depende de varios servicios y donde un error deja trabajo manual.
Una arquitectura razonable podría verse así:
- una tabla principal del negocio, por ejemplo
orders, - una tabla
workflow_statecon el paso actual y timestamps, - una tabla
workflow_outboxpara eventos confiables, - uno o más workers que leen esas tablas con locking controlado,
- métricas que midan tiempo por paso, reintentos y fallos.
El truco está en que el worker no “adivina” el estado. Lee una verdad persistida, decide el siguiente paso y actualiza todo dentro de una nueva transacción. Eso vuelve el flujo más fácil de depurar porque cada transición queda registrada con contexto.
Ejemplo de flujo de compra
Supón que vendes un curso o un producto físico. El flujo podría ser:
- El usuario confirma el checkout.
- Tu backend abre una transacción.
- Inserta el pedido y reserva inventario.
- Guarda el paso
awaiting_payment_capture. - Escribe un evento en outbox.
- Commit.
- Un worker toma el evento y llama al proveedor de pagos.
- Otro paso actualiza el pedido a
paidopayment_failed.
Con este diseño, el checkout no depende de que el proveedor de pagos responda en tiempo real para persistir el pedido. Tu sistema siempre sabe qué pasó antes y qué falta después.
Tabla resumen
| Pregunta corta | Respuesta corta |
|---|---|
| ¿Qué problema resuelve? | Evita estados intermedios frágiles en workflows distribuidos. |
| ¿Dónde vive la verdad del flujo? | En Postgres, junto con los datos del negocio. |
| ¿Qué patrón ayuda más? | Outbox, jobs table y workflow state dentro de transacciones. |
| ¿Cómo evitas duplicados? | Con locking como FOR UPDATE SKIP LOCKED. |
| ¿Qué no debes hacer? | Llamar APIs externas dentro de la transacción. |
| ¿Cuándo no alcanza? | Cuando necesitas coordinación entre múltiples bases o altísimo volumen. |
Tabla resumen
| Pregunta corta | Respuesta corta |
|---|---|
| ¿Postgres reemplaza una cola? | No siempre, pero sí puede coordinar trabajo de forma confiable. |
| ¿Sirve para equipos pequeños? | Sí, porque reduce infraestructura y simplifica operación. |
| ¿Qué gana tu equipo? | Menos bugs de consistencia y más trazabilidad. |
| ¿Qué gana producto? | Menos pedidos atascados y menos reparación manual. |
| ¿Qué gana soporte? | Consultas SQL para ver el estado real del flujo. |
Si te quedas con una sola idea, que sea esta: no necesitas separar el estado del workflow de tus datos principales por defecto. Muchas veces, mover esa coordinación a Postgres te da más confiabilidad con menos piezas. Y en sistemas reales, menos piezas suele significar menos cosas que se rompen al mismo tiempo.
Preguntas frecuentes
¿Postgres puede coordinar workflows distribuidos de verdad?
¿Esto significa que ya no necesito Kafka, SQS o RabbitMQ?
¿Qué patrón conviene más para empezar?
¿Cómo evito que dos workers procesen la misma fila?
¿Hay riesgo de volver Postgres un cuello de botella?
¿Sirve para una startup o solo para sistemas grandes?
¿Qué tipo de workflows se benefician más?
Azirgo
¿Listo para construir tu Producto Digital?
Sitios web, apps móviles, software a medida y soluciones blockchain. Cuéntanos qué tienes en mente y armamos un plan claro contigo.
- Cotización clara en 48 horas
- Equipo en Ecuador, atención en español
- Desde un MVP hasta un producto en producción