Oficio Código · Prep de entrevista · Addi

¿Aguanta 100 req/s del tráfico de Addi?

La solución del challenge está bien resuelta para lo que piden. Este es el otro lado de la conversación: dónde está su límite real y cómo evoluciona a producción.

En una respiración

1Challenge vs. producción

Casi todo lo de la derecha sería sobre-ingeniería en el challenge. Ellos piden lo de la izquierda a propósito.

 ChallengeProducción a 100 req/s
InterfazCLI batchServicio HTTP / gRPC
Externosfunciones simuladas con latenciaclientes reales, con timeouts y rate limits
Persistencia1 archivo JSON local (sin DB)cache in-memory + store distribuido
Resilienciaretry acotado → revisión manual+ circuit breaker, bulkhead, backpressure
Concurrencia1 lead por vez (checks en paralelo)miles de leads concurrentes

2Arquitectura actual

El bloque en rojo es el único que no escala. El resto está bien.

%%{init: {'theme':'base','themeVariables':{'fontFamily':'Nunito','primaryColor':'#f1e9ff','primaryTextColor':'#241f3a','primaryBorderColor':'#7c3aed','lineColor':'#6b6b86','secondaryColor':'#eafce0','tertiaryColor':'#fff6d6'}}}%%
flowchart LR
    CLI["CLI batch
1 lead por vez"] --> ORQ["Orquestador
virtual threads"] ORQ -->|paralelo| REG["Registro"] ORQ -->|paralelo| JUD["Judicial"] ORQ --> RC["Compliance
cache-first + retry"] ORQ --> SC["Score > 60"] RC --> CACHE[("bureau-cache.json
archivo local")] RC --> BUR["Bureau OFAC"] style CACHE fill:#ffd9d9,stroke:#c0392b,stroke-width:3px style BUR fill:#fff3c4,stroke:#a66a00

Fig. 1 — La del challenge. Cache-first hacia el bureau (la dependencia cara): esa parte ya escala.

3Dónde revienta a 100 req/s

Primero el compute — que no es el problema. Después el I/O — que sí.

Ley de Little. Con una latencia media por lead de ~400 ms (stage 1 en paralelo ~150 + bureau ~200 + score ~50):

L = 100 req/s × 0,4 s = 40 requests en vuelo. Trivial para virtual threads (podés tener cientos de miles).

Conclusión: el compute no es el cuello de botella. Está en el I/O.

Cuello #1 · grave

El cache en archivo

put() reescribe el mapa entero en cada miss (O(n)), bajo un synchronized global, y en JDK 21 fija el carrier thread de los virtual threads. A 100 req/s: contención brutal.

Cuello #2

Sin breaker ni timeout

Si el bureau se pone lento (no caído), cada request paga el presupuesto de reintentos completo. Falta timeout por intento + circuit breaker que corte rápido.

Ya resuelto

Presión al bureau

El cache-first reduce la carga al bureau (la dependencia cara/rate-limiteada) al miss rate. Esta decisión de diseño ya escala.

%%{init: {'theme':'base','themeVariables':{'fontFamily':'Nunito','primaryColor':'#ffe3e3','primaryTextColor':'#241f3a','primaryBorderColor':'#c0392b','lineColor':'#6b6b86','actorBkg':'#f1e9ff','actorBorder':'#7c3aed','noteBkgColor':'#fff6d6','noteBorderColor':'#a66a00'}}}%%
sequenceDiagram
    participant R as 100 req/s
    participant C as FileBureauCache
    participant D as disco
    R->>C: check (miss) → put
    Note over C: synchronized global (serializa todo)
    C->>D: reescribe mapa completo + fsync + rename
    Note over C,D: O(n) por escritura · pin del carrier thread
    R-->>R: los demás requests hacen cola en el lock
    

Fig. 2 — Aun con 80% de cache-hit, 20 put/s de O(n) bajo un lock global disparan la latencia p99.

4Arquitectura a 100 req/s

No pide sharding ni nada exótico. Pide sacar el cache del archivo y poner el breaker.

%%{init: {'theme':'base','themeVariables':{'fontFamily':'Nunito','primaryColor':'#eafce0','primaryTextColor':'#241f3a','primaryBorderColor':'#3f9c00','lineColor':'#6b6b86','clusterBkg':'#fbfcff','clusterBorder':'#d8d3ee'}}}%%
flowchart TB
    LB["Balanceador"] --> S1["Servicio HTTP
virtual threads"] LB --> S2["Servicio HTTP
virtual threads"] S1 --> ORQ["Orquestador"] subgraph NODO["cada nodo"] direction LR ORQ --> CAF["Caffeine
cache L1 · TTL"] ORQ --> CB["Circuit breaker
+ timeout por intento"] end CAF -->|miss| REDIS[("Redis · cache L2
compartido · TTL")] CB --> REG["Registro"] CB --> JUD["Judicial"] CB --> BUR["Bureau OFAC"] REDIS -->|miss| BUR ORQ --> OBS["Métricas: latencia por check ·
cache hit rate · tasa manual-review"] style CAF fill:#d4f8d4,stroke:#2e7d32 style REDIS fill:#d4f8d4,stroke:#2e7d32 style CB fill:#d9ecff,stroke:#0a72c4

Fig. 3 — Servicio HTTP + Caffeine (L1) + Redis (L2) + circuit breaker + observabilidad.

5Qué cambia, componente por componente

ComponenteChallengeProducción
InterfazCLI batchServicio HTTP + virtual threads
Cachearchivo JSON, synchronized, O(n)Caffeine (L1) + Redis (L2), TTL
Resilienciaretry → manual review+ circuit breaker + timeout por intento + bulkhead
Externosfunciones simuladasclientes con rate limiting / backpressure
Idempotenciaclave por lead (reintentos sin duplicar)
Observabilidadcolumna ms en consolamétricas + tracing por check

Casi todo esto ya está listado como pending improvements en el README del repo — lo cual, en la entrevista, se lee como “identifiqué los límites por escrito”, no como “no lo pensé”.

6El número honesto

1 nodo JVM + virtual threads  →  100 req/s sin despeinarse
                                  (si el cache no es un archivo y
                                   las dependencias responden)

techo real  ≠  "Addi a 100 req/s"
techo real  =  capacidad + rate limits del bureau / registro
               ── amortiguados por el cache-first ──

100 req/s no pide sharding ni un rediseño. Pide sacar el cache del archivo y poner el circuit breaker.

La frase para la conversación

“La solución del challenge es un kernel de orquestación correcto y probado. A 100 req/s el límite es el cache-en-archivo que el enunciado me pidió sin DB, y la falta de circuit breaker. En producción lo saco a un servicio HTTP con virtual threads, reemplazo el archivo por Caffeine + Redis con TTL, agrego circuit breaker y timeout por intento sobre el bureau, backpressure hacia los externos, idempotencia por lead y observabilidad. El cache-first ya reduce la carga al bureau, que es la dependencia cara — esa parte del diseño ya escala.”