====== 01 - Arquitecturas de software ======
Podemos clasificar una aplicación atendiendo a distintos criterios. Estas decisiones se combinan: por ejemplo, una aplicación puede desplegarse como monolito modular y organizarse internamente por capas con inversión de dependencias.
===== 1. Principios de arquitectura limpia =====
Las arquitecturas limpias protegen las reglas de negocio frente a los detalles técnicos y externos.
* **Independencia de frameworks:** el framework es una herramienta, no el centro de la aplicación.
* **Testeable:** las reglas de negocio pueden probarse sin levantar la interfaz, la base de datos ni el framework.
* **Independencia de la interfaz de usuario:** la misma lógica puede exponerse mediante interfaces distintas.
* **Independencia de la base de datos:** se puede sustituir la tecnología de persistencia sin reescribir las reglas de negocio.
* **Independencia de agentes externos:** las reglas de negocio no dependen de servicios externos concretos.
* **Regla de dependencia:** las dependencias del código apuntan hacia las políticas y reglas de negocio, no hacia los detalles.
* **Inversión de dependencias (DIP):** las abstracciones que necesita el núcleo pertenecen al núcleo; los detalles externos las implementan.
@startuml
allowmixing
package "controller" #A5D6A7 {
class Controller
}
package "domain" #EF5350 {
class UseCase
class Model
interface RepositoryPort
}
package "persistence" #90CAF9 {
class RepositoryAdapter
}
database "Base de datos" as db
Controller --> UseCase : invoca
UseCase --> Model : aplica reglas
UseCase --> RepositoryPort : usa abstraccion
RepositoryAdapter ..|> RepositoryPort : implementa
RepositoryAdapter --> db : accede
@enduml
**Arquitectura limpia** es un concepto general: busca proteger las reglas de negocio y dirigir las dependencias hacia el núcleo. La arquitectura hexagonal, la arquitectura de cebolla y la arquitectura por capas con inversión de dependencias (DIP) son distintos enfoques de arquitectura limpia.
===== 2. Organización interna =====
==== 2.1 Arquitectura por capas ====
Organiza el código según responsabilidades. En aplicaciones web es habitual distinguir estas capas:
^ Capa ^ Responsabilidad ^
| ''controller'' | Recibir solicitudes, coordinar la entrada y producir respuestas |
| ''domain'' | Contener servicios, casos de uso y reglas del negocio |
| ''persistence'' | Implementar el acceso a datos y comunicarse con la base de datos |
En una arquitectura por capas tradicional, ''controller'' llama a ''domain'' y este utiliza ''persistence''. Si el dominio depende directamente de clases concretas de persistencia, queda acoplado a la tecnología.
Cuando ''domain'' define las interfaces que necesita y ''persistence'' las implementa, se invierte la dependencia respecto a la persistencia y se aplican principios de arquitectura limpia.
Las capas son una división lógica, no una estructura rígida. Se pueden añadir capas o separar componentes según las necesidades; por ejemplo, los casos de uso pueden estar separados de los servicios de dominio.
@startuml
allowmixing
package "controller" #A5D6A7 {
class Controller
}
package "domain" #EF5350 {
class Service
class Model
interface RepositoryPort
}
package "persistence" #90CAF9 {
class RepositoryAdapter
}
database "Base de datos" as db
Controller --> Service
Service --> Model
Service --> RepositoryPort : usa
RepositoryAdapter ..|> RepositoryPort : implementa
RepositoryAdapter --> db
@enduml
==== 2.2 Arquitectura hexagonal (Ports and Adapters) ====
Separa el núcleo de la aplicación de los sistemas externos mediante puertos y adaptadores.
* **Puerto:** abstracción que describe cómo se usa el núcleo o qué necesita del exterior. Por ejemplo, una interfaz de repositorio.
* **Adaptador:** conecta un puerto con una tecnología concreta. Por ejemplo, un ''controller'' REST o una implementación JPA.
* **Dirección de dependencia:** el adaptador implementa o invoca el puerto; el dominio no conoce el adaptador.
@startuml
allowmixing
package "Adaptadores de entrada" #A5D6A7 {
class Controller
}
package "domain" #EF5350 {
interface UseCase
class Model
interface RepositoryPort
interface NotificationPort
}
package "persistence" #90CAF9 {
class RepositoryAdapter
}
package "Adaptador de servicio externo" #90CAF9 {
class EmailAdapter
}
database "Base de datos" as db
cloud "Servicio de correo" as mail
Controller --> UseCase : invoca
UseCase --> Model : aplica reglas
UseCase --> RepositoryPort : usa puerto
RepositoryAdapter ..|> RepositoryPort : implementa
RepositoryAdapter --> db
UseCase --> NotificationPort : usa puerto
EmailAdapter ..|> NotificationPort : implementa
EmailAdapter --> mail
@enduml
==== 2.3 Comparativa de enfoques internos ====
^ Enfoque ^ Idea principal ^ Observación ^
| Capas tradicional | Agrupar por responsabilidad y delegar entre capas | Sencillo; vigilar el acoplamiento hacia infraestructura |
| Hexagonal | Aislar el núcleo mediante puertos y adaptadores | Facilita sustituir tecnologías y probar el núcleo |
| Cebolla | Organizar el sistema en anillos alrededor del dominio | Las dependencias apuntan hacia el centro |
===== 3. Arquitectura de despliegue =====
Describe cómo se empaqueta, ejecuta y despliega la aplicación. Es un criterio distinto de la organización interna.
==== 3.1 Monolito, monolito modular y microservicios ====
^ Estilo ^ Despliegue ^ Límites y datos ^ Ventajas y costes ^
| Monolito | Una aplicación desplegable | Los componentes suelen compartir proceso y almacenamiento | Operación sencilla; puede crecer con alto acoplamiento |
| Monolito modular | Una aplicación desplegable | Módulos con responsabilidades, datos encapsulados y APIs internas explícitas | Mantiene simplicidad operativa y mejora la modularidad |
| Microservicios | Varios servicios desplegables | Cada servicio controla sus datos; se comunican por red | Despliegue y escalado independientes; más complejidad operativa |
* **Monolito** no significa necesariamente código desordenado: puede tener módulos bien diseñados. Es una opción adecuada para aplicaciones pequeñas y poco complejas, como un CRUD sencillo.
* **Monolito modular:** los módulos colaboran dentro de un mismo proceso mediante APIs explícitas. Las fronteras limitan dependencias y acceso a datos internos.
* **Microservicio:** servicio autónomo que se despliega por separado y es responsable de sus datos. No basta con separar una aplicación en varios proyectos.
No hay que confundir **cómo se organiza el código** (por capas, hexagonal, cebolla) con **cómo se despliega** (monolito, monolito modular, microservicios). Un monolito puede aplicar principios de arquitectura limpia.
@startuml
allowmixing
node "Monolito: un despliegue" {
component "Aplicacion\n(modulos juntos)" as monolith
database "Base de datos" as monodb
monolith --> monodb
}
node "Monolito modular: un despliegue" {
package "Aplicacion" {
component "Modulo A\n(API publica)" as moduleA
component "Modulo B\n(API publica)" as moduleB
}
database "Base de datos compartida\n(datos propiedad de cada modulo)" as modulardb
moduleA --> modulardb : solo sus datos
moduleB --> modulardb : solo sus datos
moduleB --> moduleA : API interna
}
node "Microservicios: varios despliegues" {
component "Servicio A" as serviceA
component "Servicio B" as serviceB
component "Servicio C" as serviceC
database "BD A" as dbA
database "BD B" as dbB
database "BD C" as dbC
serviceA --> dbA
serviceB --> dbB
serviceC --> dbC
serviceC --> serviceB : HTTP / mensaje
}
@enduml
Microservicios no son simplemente módulos separados en carpetas o proyectos: cada servicio debe poder desplegarse y evolucionar de forma autónoma, y la comunicación pasa por la red.
==== 3.2 Tipos de comunicación ====
La comunicación entre componentes puede ser síncrona o asíncrona. La forma concreta depende de si los componentes están dentro del mismo proceso o son servicios distribuidos.
^ Tipo ^ Funcionamiento ^ Características ^
| Síncrona | El emisor hace una llamada y espera una respuesta | Directa; el emisor depende de la disponibilidad y el tiempo de respuesta del receptor |
| Asíncrona | El emisor publica un evento o mensaje y no espera a que se complete su procesamiento | Desacopla temporalmente; puede haber retrasos, reintentos y entregas duplicadas |
=== Comunicación entre módulos ===
* Los módulos de una misma aplicación pueden comunicarse de forma **síncrona**, mediante llamadas directas a las APIs públicas de otros módulos.
* También pueden comunicarse de forma **asíncrona**, publicando eventos que otros módulos consumen.
* Los eventos internos no implican comunicación de red ni crean por sí solos aislamiento o una transacción independiente.
=== Comunicación entre microservicios ===
* Los microservicios se comunican a través de la **red**.
* La comunicación **síncrona** suele realizarse mediante peticiones HTTP y respuestas.
* La comunicación **asíncrona** se realiza publicando mensajes o eventos en un intermediario (broker), como RabbitMQ o Kafka; los consumidores los procesan posteriormente.
* En cualquiera de los estilos distribuidos, hay que contemplar fallos de red. Con mensajería, los consumidores deben ser idempotentes para tolerar entregas duplicadas.
@startuml
actor Cliente
participant "Servicio A" as ServiceA
participant "Servicio B" as ServiceB
queue "Broker de mensajes" as Broker
participant "Consumidor" as Consumer
group Comunicacion sincrona entre modulos (mismo proceso)
ServiceA -> ServiceB : API publica (llamada directa)
ServiceB --> ServiceA : resultado
end
group Comunicacion sincrona entre microservicios (HTTP)
Cliente -> ServiceA : solicitud
ServiceA -> ServiceB : petición HTTP
ServiceB --> ServiceA : respuesta
ServiceA --> Cliente : resultado
end
group Comunicacion asincrona entre componentes (eventos)
ServiceA -> Broker : publicar evento
Broker -> Consumer : entregar evento
Consumer --> Broker : confirmar procesamiento
end
@enduml
La mensajería desacopla al emisor del momento en que se procesa el evento, pero no garantiza que cada mensaje se procese exactamente una vez. Diseña consumidores idempotentes.
Durante el curso estudiaremos las capas y sus componentes en temas separados. Como referencia, seguiremos una arquitectura por capas desplegada como monolito modular.