Diseño de un sistema completo paso a paso | Nicolás Garzón
Un sistema completo no se diseña acumulando diagramas. Se diseña manteniendo coherencia entre problema, reglas, comportamiento, datos, contratos, operación y evidencia.
Diseñaremos una plataforma multi-tenant para tiendas que centraliza pedidos, inventario y entregas. El ejemplo no pretende definir la arquitectura final de DomiSys; muestra cómo conectar artefactos sin perder trazabilidad.
Pedidos llegan por WhatsApp, llamadas y mensajes.
Datos incompletos obligan a confirmar manualmente.
Inventario se consulta en hojas separadas.
Cliente y operación desconocen el estado real.
Cancelaciones y ajustes dejan inconsistencias.
Texto
Copiar Centralizar pedidos válidos y trazables
sin permitir sobreventa
ni ocultar fallos de pago o entrega.
Porcentaje de pedidos incompletos.
Tiempo desde recepción hasta confirmación.
Cancelaciones por falta de stock.
Diferencias entre stock registrado y físico.
Solicitudes de soporte por estado desconocido.
Business owner: rentabilidad, control y adopción.
Store manager: continuidad operativa.
Inventario: exactitud y trazabilidad.
Caja: velocidad y reglas claras.
Domicilios: asignación y estados.
Cliente: confirmación y seguimiento.
Soporte: diagnóstico y recuperación.
Customer.
Cashier.
Store manager.
Delivery staff.
Payment provider.
Notification provider.
Un stakeholder puede no operar directamente el sistema y un actor externo puede no ser una persona.
Texto
Copiar OBJ-01 Reducir pedidos incompletos.
OBJ-02 Evitar stock negativo por concurrencia.
OBJ-03 Hacer visible el ciclo de vida del pedido.
OBJ-04 Recuperar operaciones inciertas sin duplicar efectos.
Crear pedido.
Validar datos y sucursal.
Reservar stock.
Registrar pago o pago contra entrega.
Preparar, asignar y entregar.
Cancelar con compensaciones.
Consultar estado y auditoría.
Optimización automática de rutas.
Nómina.
Contabilidad general.
Marketplace entre negocios.
Texto
Copiar RF-01
El sistema debe permitir crear un pedido con sucursal,
cliente, dirección, líneas y método de pago.Texto
Copiar RNF-03
El 95 % de las creaciones debe responder en menos de 800 ms
bajo 50 solicitudes por segundo,
excluyendo confirmación asíncrona del proveedor de pago.Texto
Copiar RB-01 quantity > 0
RB-02 un producto debe pertenecer al tenant del pedido
RB-03 no se confirma sin reserva válida
RB-04 repetir la misma idempotency key no crea otro pedido
RB-05 cancelar una operación pagada inicia compensaciónTexto
Copiar Como cajero
quiero registrar un pedido completo
para que operación pueda prepararlo sin confirmar datos por otro canal.gherkin
Copiar Given una sucursal activa y productos disponibles
When el cajero confirma un pedido válido
Then el sistema registra un único pedido
And reserva las cantidades
And devuelve un identificador trazablegherkin
Copiar Given la misma idempotency key ya procesada
When se repite la solicitud
Then no se duplica el pedido ni la reserva
And se devuelve el resultado lógico anteriorPrecondiciones: sesión válida, caja habilitada y sucursal activa.
Garantía mínima: ningún fallo deja stock descontado sin pedido trazable.
Garantía de éxito: pedido y reserva quedan confirmados una sola vez.
Alternativas: stock insuficiente, pago rechazado, timeout, solicitud duplicada y dependencia no disponible.
mermaid
Copiar flowchart TD
Start((Inicio)) --> Capture[Capturar pedido]
Capture --> Validate{¿Datos válidos?}
Validate -->|No| Correct[Solicitar corrección]
Validate -->|Sí| Reserve[Reservar stock]
Reserve --> Available{¿Reserva exitosa?}
Available -->|No| Reject[Informar falta de stock]
Available -->|Sí| Payment{¿Requiere pago inmediato?}
Payment -->|No| Confirm[Confirmar pedido]
Payment -->|Sí| Charge[Solicitar cobro]
Charge --> Approved{¿Resultado conocido?}
Approved -->|Aprobado| Confirm
Approved -->|Rechazado| Release[Liberar reserva]
Approved -->|Incierto| Pending[Dejar operación pendiente y reconciliar]
Confirm --> Notify[Notificar]
Notify --> End((Fin))mermaid
Copiar stateDiagram-v2
[*] --> Draft
Draft --> PendingPayment : submit [stockReserved]
Draft --> Confirmed : submit [cashOnDelivery && stockReserved]
PendingPayment --> Confirmed : paymentApproved
PendingPayment --> PaymentFailed : paymentRejected
PendingPayment --> PaymentUnknown : timeout
PaymentUnknown --> Confirmed : reconcileApproved
PaymentUnknown --> PaymentFailed : reconcileRejected
Confirmed --> Preparing : startPreparation
Preparing --> Ready : completePreparation
Ready --> InDelivery : dispatch
InDelivery --> Delivered : confirmDelivery
Draft --> Cancelled : cancel
Confirmed --> Cancelling : cancel [authorized]
Cancelling --> Cancelled : compensationCompleted
Cancelling --> CancellationFailed : compensationFailedLos estados PaymentUnknown y Cancelling hacen visible trabajo pendiente que no debe representarse como éxito o fracaso prematuro.
Order: identidad y ciclo de vida.
OrderLine: producto, cantidad y precio acordado.
Money: moneda y cantidad válida.
Address: datos de entrega validados.
StockReservation: unidades retenidas temporalmente.
PaymentAttempt: cada intento y su resultado.
DeliveryAssignment: relación entre pedido y repartidor.
El total coincide con sus líneas y ajustes.
Una línea tiene cantidad positiva.
Una reserva pertenece a pedido, sucursal y tenant compatibles.
Un pedido entregado no vuelve a preparación.
Una compensación no se ejecuta dos veces.
mermaid
Copiar erDiagram
TENANT ||--o{ BRANCH : owns
TENANT ||--o{ PRODUCT : owns
BRANCH ||--o{ INVENTORY_ITEM : stores
PRODUCT ||--o{ INVENTORY_ITEM : tracked_as
BRANCH ||--o{ ORDER : receives
ORDER ||--|{ ORDER_LINE : contains
ORDER ||--o{ STOCK_RESERVATION : protects
ORDER ||--o{ PAYMENT_ATTEMPT : has
ORDER ||--o| DELIVERY_ASSIGNMENT : receivesDecisiones físicas importantes:
Unique (tenant_id, sku).
Unique (tenant_id, idempotency_key) para operación relevante.
Constraint de cantidad no negativa donde corresponda.
Control de versión para conflictos de inventario.
Historial de movimientos como evidencia, no solo saldo actual.
Texto
Copiar POST /orders
Idempotency-Key: <uuid>
Authorization: Bearer <token>JSON
Copiar {
"branchId" : "b1" ,
"customerId" : "c1" ,
"items" : [
{ "productId" : "p1" , "quantity" : 2 }
] ,
"paymentMethod" : "cash_on_delivery"
}
201: pedido creado.
200: repetición idempotente con resultado existente.
409: stock insuficiente o conflicto de versión.
422: datos válidos sintácticamente pero contrarios a reglas.
503: dependencia no disponible cuando no existe alternativa segura.
Los errores incluyen código estable, detalle, correlation ID y campos afectados.
mermaid
Copiar sequenceDiagram
autonumber
actor Cashier
participant Web
participant API as Order API
participant Inventory
participant DB
participant Outbox
Cashier->>Web: Confirma pedido
Web->>API: POST /orders + Idempotency-Key
API->>API: Autorizar y validar
API->>Inventory: Reservar cantidades
Inventory->>DB: Actualización condicional
DB-->>Inventory: Reserva creada
Inventory-->>API: reservationId
API->>DB: Insertar pedido, líneas y outbox
DB-->>API: Commit
API-->>Web: 201 Created
Outbox->>DB: Leer evento confirmado
Outbox-->>Outbox: Publicar OrderCreatedLa publicación usa outbox para evitar confirmar base de datos y perder el evento entre dos operaciones separadas.
Texto
Copiar Order Controller
→ traduce HTTP, identidad y validación superficial
Place Order
→ coordina caso de uso
Order Domain
→ protege reglas y estados
Inventory Port
→ contrato de reserva y liberación
Repositories
→ persistencia, sin decidir políticas de negocio
Outbox Publisher
→ entrega eventos confirmados
Reconciliation Worker
→ resuelve pagos o compensaciones inciertas
Tenant obtenido del contexto autenticado, no confiado desde el body.
Autorización por acción, sucursal y ownership.
MFA para acciones administrativas sensibles.
PII minimizada en eventos y logs.
Secretos fuera de imágenes y repositorio.
Auditoría de cancelaciones, descuentos y cambios de estado.
Rate limiting e idempotencia para reducir abuso y duplicados.
Texto
Copiar 20 000 pedidos/día
factor pico 12
10 lecturas por pedido
2 KB por pedido base, sin auditoría ni índicesTexto
Copiar escrituras promedio ≈ 0.23/s
pico estimado ≈ 3/s
lecturas promedio ≈ 2.3/s
pico estimado ≈ 28/sLos números no justifican microservicios por escala. Sí justifican medir carreras de inventario y picos por horarios.
Pago excede timeout: estado incierto + reconciliación; no repetir cobro ciegamente.
Notificación falla: pedido permanece confirmado y se reintenta por cola.
Inventario no disponible: rechazar rápido o degradar según regla; no aceptar venta sin garantía definida.
Worker reinicia: mensaje reprocesable e idempotente.
Base falla después del commit: respuesta incierta resuelta mediante idempotency key.
RTO y RPO se definen por capacidad. Restaurar backups se practica, no se asume.
Tasa de creación correcta.
Latencia p95 del endpoint.
Pedidos en estado incierto más de cinco minutos.
Compensaciones fallidas.
Stock negativo detectado.
Lag de outbox y reconciliación.
Cada operación conserva request_id, trace_id, tenant_id, order_id cuando existe y versión desplegada, evitando PII innecesaria.
Razón: escala moderada, equipo pequeño y necesidad de transacciones locales claras.
Consecuencia positiva: menor coste operativo.
Consecuencia negativa: requiere límites internos disciplinados y dificulta escalado independiente si aparece una necesidad real.
Razón: separar unidades disponibles de pedidos aún no confirmados.
Coste: expiración, liberación y reconciliación.
Razón: consistencia entre commit y publicación.
Coste: worker, lag, deduplicación y monitoreo.
Riesgo prioritario: doble reserva.
Texto
Copiar Pregunta
→ ¿La estrategia de control evita stock negativo con 100 solicitudes concurrentes?
Criterio
→ stock nunca negativo
→ reservas exitosas no exceden disponibilidad
→ conflictos terminan en tiempo aceptable
Conectividad inestable de sucursales.
Pago incierto.
Aislamiento multi-tenant.
Backfill de datos heredados.
Operación manual que no adopta nuevos estados.
Unitarias para totales, reglas y transiciones.
Integración para transacciones, constraints y outbox.
Concurrencia para reservas.
Contrato para API y eventos.
E2E para creación, entrega y cancelación crítica.
Recovery test para restore y reconciliación.
Seguridad para autorización entre tenants.
Carga con distribución por sucursal y horario.
Texto
Copiar OBJ-02 evitar sobreventa
→ RF-07 reservar stock
→ RB-03 no confirmar sin reserva
→ CU-04 registrar pedido
→ StockReservation + InventoryItem
→ POST /orders
→ Inventory Port + transacción condicional
→ POC concurrente + integration test
→ métrica stock_negative_total
Los objetivos tienen métricas.
El alcance y exclusiones están visibles.
Happy path, errores e incertidumbre están modelados.
Estados, reglas y contratos no se contradicen.
El ERD conserva tenant e integridad.
Los riesgos críticos tienen prueba o aceptación.
Rollout, operación y recuperación tienen owner.
El diseño completo es una cadena, no una colección.
Cada artefacto debe responder una pregunta real.
Los estados inciertos son parte del dominio distribuido.
Datos, contratos y operación deben preservar las mismas reglas.
La evidencia cierra el diseño.
¿Por qué PaymentUnknown es mejor que devolver rechazo tras un timeout?
¿Qué problema resuelve el outbox?
¿Por qué la escala estimada no obliga a microservicios?
¿Cómo se traza la regla de no sobreventa hasta producción?
Ver respuestas
Porque el proveedor puede haber procesado el cobro; declarar rechazo permitiría repetir y duplicar efectos.
Evita la ventana entre confirmar datos y publicar el evento, conservando una entrega reintentable.
La forma arquitectónica depende también de equipo, consistencia, operación y drivers; la carga es manejable con una solución simple.
Requisito, regla, reserva, constraint o control concurrente, pruebas de carrera y métrica de stock negativo.
Antipatrones de System Design ayuda a reconocer atajos que rompen esta coherencia.