Saltar a contenido

Trade Offs Arquitectonicos

Análisis de las decisiones arquitectónicas clave de Camunda 8/Zeebe, sus razones, qué alternativas descartaron, y qué implicaciones tiene cada decisión para una plataforma simplificada.

1. Command Sourcing vs Event Sourcing Puro

Decisión: Commands se escriben al log ANTES de procesarse (command sourcing), no solo los eventos resultantes.

Por qué: garantiza que todos los nodos procesan los mismos commands en el mismo orden. En event sourcing puro, el leader genera eventos y los replica; si el leader falla mid-processing, hay ambigüedad sobre qué eventos se generaron. Con command sourcing, cualquier follower puede re-derivar los eventos determinísticamente.

Trade-off: el log contiene commands que pueden ser rechazados (COMMAND_REJECTION), aumentando el volumen del log. Pero elimina edge cases de inconsistencia en failover.

Para una plataforma simplificada: mantener command sourcing. La complejidad de implementar es similar al event sourcing, pero la resiliencia es significativamente mayor. Alternativa viable solo si se descarta clustering.

2. Single-threaded Processing per Partition

Decisión: cada partición se procesa en un solo thread (actor model), sin locks ni concurrency.

Por qué: elimina toda la complejidad de concurrencia — no hay race conditions, no hay deadlocks, no hay optimistic locking failures. El procesamiento es determinístico, lo que permite replay exacto.

Trade-off: el throughput de UNA partición está limitado por un solo core. Para escalar se necesitan más particiones (horizontal scaling vía sharding).

Para una plataforma simplificada: excelente decisión para mantener. Simplifica enormemente la implementación. Para un MVP, una sola partición con single-thread puede ser suficiente. Escalar después con partitioning.

3. RocksDB como State Store

Decisión: estado embebido en RocksDB (key-value store) en el mismo proceso del broker, no en una base de datos externa.

Por qué: latencia ultra-baja (microsegundos vs milisegundos de un DB externo). No hay network hop para lecturas de estado. Snapshots nativos para backup/restore. Column families proveen separación lógica sin overhead.

Trade-off: el estado no es directamente consultable desde fuera (necesita exporters para exponer datos). El tamaño del estado está limitado por disco local. No hay SQL/queries ad-hoc sobre el estado.

Para una plataforma simplificada: considerar alternativas más simples como SQLite (WAL mode) si no se necesita el rendimiento extremo. RocksDB requiere JNI y tuning de column families. SQLite ofrece queries SQL directas y simplifica debugging.

4. Raft para Consenso

Decisión: Raft (vía fork de Atomix) para replicación y leader election.

Por qué: Raft es el protocolo de consenso más comprensible y más implementado en producción. Garantiza linearizability. El fork de Atomix fue necesario porque la versión original tenía problemas de rendimiento y mantenimiento.

Trade-off: mantener un fork de Raft es costoso (Camunda invierte significativamente en esto). Raft requiere quorum writes, lo que añade latencia proporcional al número de réplicas.

Para una plataforma simplificada: para un MVP single-node, Raft no es necesario. Para HA, considerar usar una implementación existente (etcd embedded, o Raft libraries como hashicorp/raft en Go). NO hacer un fork propio — es la parte más costosa del mantenimiento.

5. gRPC como Protocolo Primario

Decisión: gRPC (HTTP/2) como protocolo de comunicación principal, con REST como segunda opción.

Por qué: bidirectional streaming (para StreamActivatedJobs), protocolo buffers para serialización eficiente, code generation para múltiples lenguajes, backpressure nativo via HTTP/2 flow control.

Trade-off: gRPC es menos accesible que REST para desarrolladores. Requiere HTTP/2 que complica proxying/load-balancing. Debugging es más difícil (binary protocol).

Para una plataforma simplificada: REST-first sería más pragmático. gRPC solo si se necesita streaming de jobs con alta frecuencia. WebSockets son una alternativa para streaming más simple.

6. Exporter como Pipeline de Datos

Decisión: datos salen del engine solo via exporters que leen el log (push-based after commit). No hay queries directas al engine state.

Por qué: desacopla completamente el engine de los consumidores de datos. El engine nunca se bloquea por queries lentas. Cada exporter puede fallar independientemente sin afectar el procesamiento.

Trade-off: eventual consistency entre el engine y las UIs (Operate/Tasklist ven datos con delay). El pipeline añade complejidad operacional (ES/OS como dependencia). Un exporter lento bloquea a todos los demás en la misma partición.

Para una plataforma simplificada: simplificar significativamente. Para un MVP, el state store podría ser directamente consultable (e.g., SQLite). Exporters solo para analytics/audit. Reduce dependencia de Elasticsearch.

7. Partition-Embedded Keys

Decisión: 13 bits del partition ID embebidos en el top de cada key de 64 bits.

Por qué: permite identificar el dueño de cualquier entidad con solo inspeccionar su key, sin consultar un registry. Esencial para command distribution cross-partition y para detectar si un command fue distribuido.

Trade-off: limita a 8192 particiones y 2^51 keys per partition. Complica key generation (necesita partition-aware).

Para una plataforma simplificada: mantener si se planea multi-partition. Si es single-partition, usar keys simples monotónicos.

8. Composition over Inheritance en Processors

Decisión: 24 element processors comparten lógica via ~12 Behavior classes en vez de herencia.

Por qué: los BPMN elements tienen combinaciones ortogonales de capacidades (algunos tienen jobs, algunos tienen events, algunos tienen variables). Herencia crearía una jerarquía frágil (diamond problem). Composition permite mix-and-match.

Trade-off: más boilerplate en cada processor (inyectar behaviors). Menos discoverable para nuevos desarrolladores.

Para una plataforma simplificada: mantener este patrón. Es la decisión correcta para un workflow engine. Reducir el número de behaviors al mínimo necesario para el subset BPMN soportado.

9. Dropping vs Buffering para Backpressure

Decisión: bajo carga, rechazar commands (dropping) en vez de bufferearlos.

Por qué: buffering eventualmente causa OOM. Dropping traslada la responsabilidad al cliente (retry with backoff). El sistema se auto-regula: cuando la carga baja, los clients reintentan exitosamente.

Trade-off: el cliente debe implementar retry logic. Commands críticos (cancel, complete) se whitelistan para nunca ser rechazados — esto previene deadlocks donde el sistema no puede hacer progreso.

Para una plataforma simplificada: mantener. Simple y robusto. El whitelisting de commands críticos es esencial y no obvio — documentar bien.

10. Elasticsearch como Store Secundario

Decisión: Elasticsearch/OpenSearch como backend de búsqueda para todas las UIs.

Por qué: queries complejas (full-text, aggregations, time-series) que RocksDB no puede hacer. Indexación flexible. Bien entendido por el ecosistema.

Trade-off: ES es operacionalmente complejo, consume muchos recursos, y es un single point of failure para las UIs. El mapping management (ILM, templates, index rotation) añade complejidad.

Para una plataforma simplificada: considerar PostgreSQL como backend único (state + search). Con índices apropiados, Postgres puede manejar la mayoría de queries que Operate/Tasklist necesitan. Elimina la dependencia de ES completamente.

Resumen de Prioridades para Simplificación

Decisión Mantener Simplificar Eliminar
Command sourcing
Single-threaded processing
RocksDB state store ✅ SQLite
Raft consensus ✅ Library Para MVP
gRPC protocol ✅ REST-first
Exporter pipeline ✅ Direct queries
Partition-embedded keys Para MVP
Composition behaviors
Dropping backpressure
Elasticsearch ✅ PostgreSQL