Cómo diseño backends Java que no se rompen: 10 principios pragmáticos (sin dogmas)
10 principios prácticos que uso para diseñar backends Java que aguantan producción. No es una religión. Es una lista de decisiones que reducen sorpresas.
Si has trabajado en sistemas Java “de verdad”, sabes que los incidentes casi nunca vienen de “no usamos el patrón correcto”, sino de cosas más aburridas: una integración que reintenta, un timeout mal puesto, un NullPointerException que se comió el contexto, un endpoint que no es idempotente, un mensaje duplicado, un release que no puedes observar.
Este post es mi punto de partida: 10 principios prácticos que uso para diseñar backends Java que aguantan producción. No es una religión. Es una lista de decisiones que reducen sorpresas.
1) Contratos explícitos antes que “DTOs que se entienden por fe”
Regla: define el contrato (inputs/outputs/errores) de forma explícita y estable.
Por qué: el “contrato real” no es tu controller; es lo que consumen otros equipos. Si el contrato vive solo en el código, cambia sin querer.
Práctica:
- OpenAPI como fuente de verdad (aunque lo generes desde anotaciones, que el output sea revisable).
- Versiona el contrato (aunque sea “v1” en la ruta o en el header).
Anti-patrón: “lo cambio rápido, total nadie usa ese campo” → sí lo usan, solo no lo sabías.
2) Idempotencia por defecto en comandos y endpoints mutables
Regla: cualquier operación que crea o cobra o dispara algo externo debe ser idempotente.
Por qué: en producción todo se reintenta: gateways, SDKs, balanceadores, usuarios, jobs. Si tu operación no es idempotente, duplicas efectos.
Práctica:
- Header
Idempotency-Key+ almacenamiento de resultado por llave (con TTL). - En mensajería: deduplicación por
messageId/eventIden tu storage.
Anti-patrón: “eso solo pasa si el cliente está mal implementado”.
3) Los errores son parte del dominio, no un “stacktrace”
Regla: modela fallas con estructura, no con strings.
Por qué: un RuntimeException es barato para escribir y carísimo para operar. Sin estructura no puedes agregar métricas, automatizar alertas, ni ayudar al usuario.
Práctica:
- Respuestas de error consistentes (tipo Problem JSON o un
ErrorResponseconcode,message,reason,details,traceId). - Distingue errores de validación, de negocio, infra, no encontrado, conflicto.
Anti-patrón: “loggear y relanzar” hasta que se pierda el contexto.
4) Consistencia explícita: decide dónde necesitas ACID y dónde no
Regla: no te escondas detrás de “eventual consistency” ni de “todo en una transacción”.
Por qué: ambos extremos generan sistemas frágiles. La clave es declarar qué garantías necesita cada operación.
Práctica:
- Ledger/contabilidad: transacciones fuertes.
- Integraciones: outbox + reintentos + idempotencia.
Anti-patrón: publicar eventos “a mano” dentro de una transacción sin outbox.
5) Outbox pattern para integrar sin perder mensajes
Regla: si actualizas DB y luego notificas afuera, hazlo con outbox.
Por qué: “guardar en DB y luego mandar” falla en el peor momento: guardaste, no enviaste, nadie sabe, y ya no puedes reconstruir.
Práctica:
- En la misma transacción: insertas tu cambio + insertas fila en
outbox. - Un worker lee outbox y publica; marca enviado.
Anti-patrón: “lo mando directo a Kafka/SQS y si falla pues… reintento en memoria”.
6) Observabilidad desde el día 1: logs útiles, métricas mínimas, trazas cuando duela
Regla: si no lo puedes medir, no lo puedes operar.
Por qué: el bug más caro es el que “no puedes reproducir”. Observabilidad es lo que te permite entender, no solo “ver que falló”.
Práctica:
- Logs estructurados (JSON) con
traceId,requestId,userId(si aplica),operation. - Métricas: latencia, errores por código, throughput, colas, retries.
- Trazas distribuídas en integraciones (cuando hay microservicios o colas).
Anti-patrón: logs “bonitos” sin IDs correlacionables.
7) Timeouts y retries como diseño, no como accidente
Regla: todo I/O debe tener timeout; todo retry debe tener backoff y límite.
Por qué: sin timeouts, tu sistema se muere por saturación. Sin backoff, haces DDoS involuntario a tu dependencia.
Práctica:
- Timeout por cliente HTTP y por llamada.
- Retry solo en errores transitorios (y con jitter).
- Circuit breaker cuando la dependencia está enferma.
Anti-patrón: retry infinito “porque así se recupera”.
8) “No hay magia”: separa dominio de infraestructura (aunque no seas purista)
Regla: que tu dominio no dependa de frameworks.
Por qué: cuando todo está pegado a Spring/JPA, el sistema se vuelve difícil de probar, evolucionar y entender.
Práctica:
- Dominio: clases simples (POJOs/records), reglas, servicios de dominio.
- Infra: adaptadores (repositorios, clients HTTP, mensajería).
- Interfaces en el dominio, implementaciones en infra.
Anti-patrón: entidades JPA como “modelo universal” + lógica de negocio en @Entity.
9) Concurrencia consciente: locks, versionado y conflictos claros
Regla: si hay competencia por el mismo recurso, asúmelo y manéjalo.
Por qué: en pagos, inventarios, saldos, etc. los conflictos no son “edge cases”, son el caso normal.
Práctica:
- Optimistic locking (
@Version) cuando esperas pocos choques. - Pessimistic locking cuando el costo del conflicto es alto.
- Errores
409 Conflicto códigos de negocio cuando no se puede aplicar un comando.
Anti-patrón: “último write gana” sin que nadie se entere.
10) Testing por capas: menos mocks, más contratos y entornos reales
Regla: prueba lo que te puede romper en producción, no solo lo que es fácil de testear.
Por qué: el 80% de los bugs vienen de integración, configuración, datos, concurrencia, serialización… justo lo que el unit test aislado no cubre.
Práctica:
- Unit tests para reglas puras (cero I/O).
- Integration tests con DB real (Testcontainers) para repos y transacciones.
- Contract tests para APIs/mensajería cuando hay múltiples consumidores.
- Smoke tests en despliegue.
Anti-patrón: 500 unit tests con mocks y cero pruebas de DB/migraciones.
Si solo haces 3 cosas esta semana…
- Agrega Idempotency-Key a tus endpoints de “crear/cobrar/disparar”.
- Implementa outbox para cualquier publicación de eventos post-DB.
- Estandariza ErrorResponse + traceId y logs estructurados.
Cierre
Este manifiesto es mi base. De aquí salen posts más específicos: idempotencia en Spring, outbox con JPA, modelado de errores sin excepciones “por todo”, testing con Testcontainers, y cómo dividir un monolito en módulos con límites reales.
Si estás armando un backend Java que procesa operaciones “serias” (pagos, órdenes, ledger, integraciones), estos 10 principios te ahorran dolor.
¿Preguntas o comentarios? Escríbeme o conecta conmigo en LinkedIn.