Fecha de publicación: 2023-11-24
En el mundo de los microservicios, no todo es un endpoint REST. Algunos procesos necesitan ejecutarse en segundo plano, sin depender de una solicitud HTTP. Es ahí donde entran los workers. Entender la diferencia entre un servicio API y un worker es clave para diseñar sistemas escalables.
¿Qué es un servicio API?
Un servicio API es un proceso que expone endpoints HTTP o gRPC, recibe solicitudes, las procesa y devuelve una respuesta de forma sincrónica (o cuasi-sincrónica). Son el punto de entrada para que clientes, navegadores u otros servicios interactúen con tu sistema.
Ejemplos habituales: una REST API, un servicio GraphQL, un servidor gRPC. Sus características definitorias son:
- Modelo request/response: cada solicitud espera una respuesta.
- La latencia importa: el usuario o servicio que consume la API espera una respuesta rápida.
- Idealmente stateless: no guarda estado entre solicitudes, lo que facilita el escalamiento.
- Escalable horizontalmente detrás de un load balancer.
¿Qué es un Worker?
Un worker es un proceso que consume tareas de una cola (Kafka, RabbitMQ, SQS) o ejecuta trabajos programados mediante cron. No expone endpoints HTTP al exterior; su trabajo es procesar mensajes de manera asíncrona.
Sus características definitorias son:
- Procesamiento asíncrono: no hay nadie esperando una respuesta inmediata.
- El throughput importa más que la latencia: se optimiza para procesar volúmenes altos.
- Puede ser stateful durante el procesamiento de un mensaje, aunque no entre mensajes.
- Escalable horizontalmente agregando más instancias consumidoras a la misma cola.
Comparativa rápida
| Aspecto | API | Worker |
|---|---|---|
| Trigger | Request HTTP | Mensaje en cola / Cron |
| Respuesta | Sincrónica | No hay respuesta directa |
| Latencia | Crítica | Secundaria |
| Throughput | Moderado | Alto |
| Escalamiento | Load balancer | Más consumidores |
| Ejemplo | GET /api/users | Procesar 10K emails |
Ejemplo práctico: sistema de notificaciones
El caso de las notificaciones es un ejemplo clásico donde la separación entre API y worker brilla con claridad. La API recibe la solicitud del usuario y la encola de inmediato; el worker procesa cada notificación de forma independiente y con la capacidad de reintentar si algo falla.
La API recibe la solicitud:
kotlin
1@RestController
2@RequestMapping("/api/notifications")
3class NotificationController(
4 private val kafkaTemplate: KafkaTemplate<String, String>,
5 private val objectMapper: ObjectMapper
6) {
7 @PostMapping
8 fun sendNotification(@RequestBody request: NotificationRequest): ResponseEntity<Map<String, String>> {
9 // La API solo encola el trabajo, no lo ejecuta
10 val message = objectMapper.writeValueAsString(request)
11 kafkaTemplate.send("notifications", request.userId, message)
12
13 return ResponseEntity.accepted().body(
14 mapOf("status" to "QUEUED", "message" to "Notificación encolada para procesamiento")
15 )
16 }
17}
18
19data class NotificationRequest(
20 val userId: String,
21 val type: String, // EMAIL, SMS, PUSH
22 val subject: String,
23 val body: String
24)El Worker procesa las notificaciones:
kotlin
1@Service
2class NotificationWorker(
3 private val emailService: EmailService,
4 private val smsService: SmsService,
5 private val pushService: PushService,
6 private val objectMapper: ObjectMapper
7) {
8 private val logger = LoggerFactory.getLogger(NotificationWorker::class.java)
9
10 @KafkaListener(topics = ["notifications"], groupId = "notification-workers")
11 fun processNotification(record: ConsumerRecord<String, String>) {
12 val notification = objectMapper.readValue(record.value(), NotificationRequest::class.java)
13 logger.info("Procesando notificación ${notification.type} para usuario ${notification.userId}")
14
15 when (notification.type) {
16 "EMAIL" -> emailService.send(notification)
17 "SMS" -> smsService.send(notification)
18 "PUSH" -> pushService.send(notification)
19 else -> logger.warn("Tipo de notificación desconocido: ${notification.type}")
20 }
21 }
22}¿Por qué separar API y Worker?
La separación no es solo una cuestión de estilo arquitectónico; tiene beneficios concretos y medibles:
- Resiliencia: si el envío de emails falla, la API ya respondió 202 Accepted. El worker puede reintentar sin afectar al usuario.
- Escalamiento independiente: si tienes 100K emails que enviar, escalas los workers sin tocar la API.
- Latencia predecible: la API responde en milisegundos siempre. El procesamiento pesado ocurre en background.
- Separación de responsabilidades: cada servicio tiene una sola razón para cambiar.
Patrones comunes con Workers
Al trabajar con workers, algunos patrones son casi universales en sistemas de producción:
- Retry con backoff exponencial: si falla, reintentar con delays crecientes para no sobrecargar el sistema que está fallando.
- Dead Letter Queue (DLQ): mensajes que fallan N veces van a una cola separada para revisión manual, sin bloquear el flujo principal.
- Idempotencia: el worker debe poder procesar el mismo mensaje más de una vez sin efectos duplicados. Kafka puede entregar el mismo mensaje más de una vez en ciertos escenarios.
- Batching: procesar mensajes en lotes para mayor throughput, reduciendo el overhead por operación.
¿Cuándo usar cada uno?
Usa una API cuando:
- El usuario o servicio necesita una respuesta inmediata.
- La operación es rápida (menos de 500 ms en condiciones normales).
- Necesitas validar datos y devolver errores de forma sincrónica.
Usa un Worker cuando:
- El procesamiento puede tomar segundos o minutos.
- No necesitas respuesta inmediata: solo confirmación de recepción.
- Necesitas reintentos automáticos ante fallos transitorios.
- Estás procesando volúmenes altos de mensajes o eventos.
Usa ambos cuando: la API valida y encola, el worker procesa. Este es el patrón más común en producción. La API actúa como puerta de entrada y garante de la integridad de los datos; el worker se encarga del trabajo pesado de forma desacoplada.
No todo necesita ser un endpoint. Separar la recepción de solicitudes (API) del procesamiento pesado (Worker) es uno de los patrones más poderosos y simples en arquitectura de microservicios. La clave está en reconocer qué tipo de trabajo estás modelando: si el resultado debe ser inmediato, usa una API; si puede esperar y escalar de forma independiente, usa un worker.

