Cuándo usarlos: procesos vecinos, dispositivos que se hablan y por qué esto no sustituye a una API HTTP
Los actores distribuidos brillan donde controlas los dos extremos y el estado tiene un domicilio. Fracasan como frontera pública, porque su contrato es un tipo de Swift compartido y no un protocolo negociable. El criterio para decidir, y las preguntas que lo deciden.
Toda tecnología de invocación remota atraviesa la misma fase peligrosa: funciona tan bien en la demostración que uno empieza a preguntarse si no serviría para todo. Con los actores distribuidos la tentación es especialmente fuerte, porque la ergonomía es genuinamente superior a escribir clientes y rutas a mano, y porque el código resultante es el mismo que ya sabías escribir. Conviene por eso ser explícito sobre el contrato real: distributed te da una llamada segura entre dos programas que comparten la definición del tipo, y todo su valor y todos sus límites se derivan de esa condición. Esta lección cierra el nivel convirtiendo esa frase en un criterio operativo.
- Identificar los escenarios donde un actor distribuido es la herramienta correcta y por qué.
- Explicar por qué el contrato de un actor distribuido no puede ser una frontera pública.
- Contrastar el acoplamiento por tipo compartido con el acoplamiento por protocolo negociado.
- Aplicar un criterio de decisión reproducible antes de introducir
distributeden un diseño.
Dónde encajan de verdad
Hay tres familias de casos en las que el modelo no solo funciona sino que resulta claramente mejor que la alternativa.
La primera es el proceso vecino que tú despliegas: una aplicación de escritorio con su proceso auxiliar privilegiado, una extensión aislada en una caja de arena, un demonio acompañante. Ambos binarios salen del mismo repositorio, se actualizan juntos y hablan por un canal local. Ahí el tipo compartido no es una atadura sino exactamente la garantía que quieres, y sustituye a un protocolo hecho a mano sobre XPC o sobre una tubería que nadie tendría ganas de mantener.
// Módulo compartido por la aplicación y por su proceso auxiliar
distributed actor Indexador {
typealias ActorSystem = MiSistemaLocal
private var avance: Double = 0
distributed func indexar(_ carpeta: URL) async throws -> Resumen {
try await recorrer(carpeta) { self.avance = $0 }
}
distributed func progreso() -> Double { avance }
}
El contrato es literalmente ese fichero, y el compilador lo verifica en los dos binarios a la vez. Comparado con serializar diccionarios a mano y comprobar claves en ejecución, la diferencia no es de comodidad sino de clase de error: lo que antes fallaba en producción ahora no compila.
La segunda familia son los dispositivos que se hablan entre sí dentro de un mismo producto: un teléfono y su reloj, varios equipos en la red local coordinando una sesión, un panel de control y el aparato que controla. La topología es dinámica, los participantes aparecen y desaparecen, y el estado vive donde vive el dispositivo. Es justo el terreno donde la identidad estable, el descubrimiento y la vigilancia de terminación pagan su precio.
La tercera es el servicio con estado y domicilio: una sala de juego, una sesión de edición colaborativa, el gemelo lógico de un dispositivo físico. Su rasgo definitorio es que las peticiones no son intercambiables porque deben llegar al mismo sitio.
distributed actor SalaDeJuego {
typealias ActorSystem = ClusterSystem
private var jugadores: [Jugador: Estado] = [:]
distributed func entrar(_ j: Jugador) throws -> Instantanea {
guard jugadores.count < 8 else { throw ErrorSala.llena }
jugadores[j] = .esperando
return instantanea()
}
}
Esa sala no puede atenderla cualquier réplica: solo la que tiene el estado. Un balanceador sin afinidad la rompe, y añadir afinidad al balanceador es reinventar peor lo que la identidad del actor ya resuelve.
Aunque nunca despliegues más de un proceso, modelar con distributed y correr sobre un sistema local de pruebas te obliga a escribir interfaces serializables, de grano grueso y tolerantes al fallo. Esa disciplina mejora el diseño antes de que exista ninguna red.
Por qué no sustituyen a una API HTTP
El argumento se sostiene sobre un hecho técnico del que ya vimos la mecánica: la invocación viaja identificada por el nombre decorado del método, y los argumentos se decodifican con la definición exacta del tipo. De ahí se siguen, una por una, todas las razones.
El cliente tiene que ser Swift y tiene que ser tu Swift. No hay navegador, ni curl, ni un cliente en Python escrito por otro equipo, ni una especificación que alguien pueda leer para integrarse sin hablar contigo. Una API HTTP es descriptible con un documento; un actor distribuido es descriptible con un módulo compilado.
La evolución exige desplegar los dos lados a la vez. Cambiar la firma de un distributed func no es un cambio compatible: el nombre decorado cambia y las llamadas antiguas dejan de encontrar destino. HTTP no es mágico, pero tiene un repertorio conocido —campos opcionales, versiones en la ruta, negociación de contenido, despliegues escalonados— y una cultura de compatibilidad que aquí hay que reinventar.
// Version 1, desplegada en cien clientes
distributed func reservar(_ id: UUID) throws -> Recibo
// Version 2: parece un anadido inocente y rompe a todos los clientes viejos
distributed func reservar(_ id: UUID, plazas: Int) throws -> Recibo
// La unica evolucion segura: un tipo de peticion que crece por dentro
struct Reserva: Codable, Sendable {
let id: UUID
var plazas: Int? = nil // campo nuevo, siempre opcional
}
distributed func reservar(_ peticion: Reserva) throws -> Recibo
La tercera versión es la que sobrevive, y no por casualidad: es exactamente la disciplina que impone un formato de mensajes con evolución explícita. Si acabas escribiéndola, has reconstruido a mano la mitad de lo que un contrato publicado te daba de serie, con la diferencia de que aquí nadie te obliga a hacerlo y el olvido se paga en el siguiente despliegue.
Falta toda la infraestructura de una frontera pública. Autenticación y autorización por petición, límites de caudal, caché intermedia, red de distribución, herramientas de inspección, cortafuegos que entienden el protocolo. Nada de eso viene incluido, y varias de esas piezas existen precisamente porque el otro lado no es de fiar. La observabilidad tiene mejor suerte, porque swift-distributed-tracing se integra y propaga contexto, pero es la excepción y no la regla.
La madurez importa. El paquete de clúster sigue evolucionando y no ha alcanzado una versión estable; apostar la frontera pública de un producto a esa base es una decisión que hay que tomar con los ojos abiertos y la versión fijada.
flowchart TD
A[Necesito hablar con otro proceso] --> B{Controlo ambos extremos}
B -->|No| H[API HTTP con contrato publicado]
B -->|Si| C{Se despliegan juntos}
C -->|No| H
C -->|Si| D{Hay estado con domicilio}
D -->|No| E[Servicio sin estado detras de un balanceador]
D -->|Si| F[Actores distribuidos]
style F fill:#a6e3a1,color:#11111b
style H fill:#f9e2af,color:#11111bEl criterio en cuatro preguntas
Quién es el otro extremo
Si es un equipo, un cliente o un lenguaje que no controlas, necesitas un contrato publicado, no un tipo compartido.
Cómo se despliega
Si ambos lados no pueden actualizarse en la misma ventana, el acoplamiento por firma se convierte en una avería programada.
Dónde vive el estado
Si cada petición puede atenderla cualquier réplica, no necesitas identidad ni ubicación: necesitas un servicio sin estado.
Cuánto se hablan
Si el diálogo es fino y constante, el modelo de actores encaja. Si son peticiones esporádicas y gordas, HTTP sobra y basta.
// Frontera publica: contrato negociable, versionado, autenticado
app.post("v1/reservas") { peticion in
let dto = try peticion.content.decode(ReservaDTO.self)
let sala = try SalaDeJuego.resolve(id: .nombre(dto.sala), using: sistema)
return try await sala.entrar(dto.jugador) // frontera interna: tipo compartido
}
Un cuarto matiz cierra el cuadro: las dos cosas conviven bien. El patrón habitual en sistemas serios es una frontera HTTP hacia el exterior, con su documento de contrato y su autenticación, y actores distribuidos por dentro para coordinar los procesos que ese servicio orquesta. La frontera pública protege la evolución; los actores hacen cómodo el interior. Confundir las dos capas es lo que produce, según la dirección del error, o un servicio interno con veinte capas de serialización innecesarias o una API pública que solo puede consumir su propio autor.
La pregunta de fondo del nivel no es técnica: es dónde poner la costura de un sistema y qué se está prometiendo al ponerla ahí. Un tipo compartido y un protocolo publicado resuelven el mismo problema de comunicación y presuponen mundos sociales opuestos. El tipo compartido asume un único propietario del significado: hay una definición canónica, compilada, verificada por un compilador que ve los dos lados a la vez, y esa verificación es la forma más fuerte de garantía que existe, capaz de convertir en error de compilación lo que en HTTP sería una incidencia en producción a las tres de la madrugada. El precio es que ambos extremos quedan atados al mismo ciclo de vida, y ese precio no se paga en bytes sino en grados de libertad organizativa: cada cambio requiere coordinar dos despliegues, y coordinar despliegues requiere coordinar equipos, calendarios y prioridades. El protocolo publicado hace lo contrario: renuncia a la verificación estática, acepta que los mensajes se validen al llegar y que el desajuste se manifieste en ejecución, y a cambio compra independencia evolutiva —campos que se ignoran, versiones que conviven, clientes que llevan tres años sin actualizarse y siguen funcionando. Por eso la elección refleja fielmente la ley de Conway y su corolario inverso: la arquitectura que elijas acabará dictando la estructura de equipos capaz de sostenerla, y una costura mal colocada produce, sin excepción, o bien un monolito distribuido —muchos procesos que solo pueden desplegarse juntos, es decir, todos los costes de la distribución sin ninguna de sus ventajas— o bien la fragmentación opuesta, donde una frontera negociada separa dos módulos que en realidad siempre cambian a la vez y solo añade latencia y ceremonia. La regla que se deduce es más útil que cualquier comparación de rendimiento: coloca la frontera fuerte donde esté la frontera de propiedad, y usa dentro de cada dominio la herramienta que maximiza la verificación —tipos compartidos, actores distribuidos, llamadas comprobadas por el compilador— y entre dominios la que maximiza la independencia. Los actores distribuidos de Swift son excelentes en el primer papel y, si se les asigna el segundo, no fallan por falta de calidad técnica sino por una confusión de categorías: se les pide que sean un tratado internacional cuando lo que son, y muy bien, es una conversación entre dos partes del mismo cuerpo.
Úsalos cuando controles ambos extremos, se desplieguen juntos y el estado tenga domicilio: procesos auxiliares, dispositivos de un mismo producto, servicios con identidad. No los uses como frontera pública: su contrato es un tipo compilado, no un documento negociable. Conviven bien con una API HTTP por fuera.
- Toma tres integraciones reales de tu trabajo y pásalas por las cuatro preguntas del criterio. Escribe el veredicto y el motivo dominante de cada una.
- Elige una que hoy sea HTTP entre dos procesos que despliegas juntos y esboza cómo quedaría con actores distribuidos. Cuenta las líneas que desaparecen.
- Toma la que menos encaje y enumera qué exigirías al equipo cliente si la convirtieras en actores. Esa lista es la medida del acoplamiento.
- Modela un servicio con estado por sesión y explica qué se rompe al ponerlo detrás de un balanceador sin afinidad.
- Diseña una arquitectura mixta con frontera HTTP hacia fuera y actores por dentro, y marca en el dibujo el punto exacto donde termina el contrato negociable.