Visión General de la Arquitectura
En este enfoque, el sistema se organiza en cinco capas independientes. El módulo bootstrap sirve como punto de entrada para el arranque y la configuración de componentes.
Responsabilidades por Módulo
| Módulo | Responsabilidad Principle | Restricciones |
|---|---|---|
| api | Definición de contratos de servicio (interfaces para Dubbo/Feign). | Prohibida la lógica de negocio, dependencias a otros módulos o uso de enumeraciones. |
| domain | Núcleo del dominio: entidades, objetos de valor, comandos, eventos y abstracciones de repositorio. | No puede depender de marcos tecnológicos externos. |
| app | Implementa las interfaces de api, coordina objetos de dominio y middleware; incluye consumidores MQ, tareas y lógica de orquestación. | No contiene reglas de negocio esecniales; solo depende de abstracciones de domain. |
| infrastructure | Implementa interfaces de repositorio del dominio para persistencia (DB, Redis, ES, MQ). | Prohibida la inclusión de lógica de negocio. |
| acl | Capa de aislamiento contra legado, implementa abstracciones de dominio para interacciones externas. | No permite el paso directo de DTOs externos al dominio. |
| bootstrap | Punto de inicio de la aplicación, configuración técnica y ensamblaje de servicios. | No alberga lógica de negocio. |
Dirección de Dependencias
Las dependencias apuntan siempre hacia el módulo domain. La capa app depende únicamente de las abstracciones definidas en domain, mientras que infrastructure y acl proporcionan implementaciones en tiempo de ejecución, siguiendo el principio de inversión de dependencias (DIP).
Principios Fundamentales
- Inversión de Dependencias (DIP): El dominio define interfaces; infraestructura y acl las implementan.
- Responsabilidad Única (SRP): Cada módulo tiene un propósito claro, reforzado por dependencias de compilación.
- Abierto/Cerrado (OCP): Las abstracciones permiten extensiones sin modificar el núcleo.
- Orientado al Dominio (DDD): El modelo de dominio es central; los agregados garantizan consistencia; los eventos desacoplan operaciones.
Las directrices inquebrantables son: las dependencias siempre apuntan a domain, y las reglas de negocio residen exclusivamente en domain.
Estructura del Proyecto
El siguiente árbol de directorios muestra la organización estándar del proyecto. Los fragmentos de código posteriores se alinean con esta estructura.
proyecto-ddd-springboot/
├── pom.xml
├── api/
│ └── src/main/java/com/ejemplo/api/
│ ├── InterfazUsuario.java
│ ├── Respuesta.java
│ ├── dto/
│ │ ├── UsuarioDTO.java
│ │ ├── ComandoCrearUsuario.java
│ │ └── ComandoActualizarUsuario.java
│ └── consulta/
│ └── ConsultaUsuario.java
├── domain/
│ └── src/main/java/com/ejemplo/domain/
│ ├── modelo/
│ │ ├── Usuario.java
│ │ ├── IdentificadorUsuario.java
│ │ ├── NombreUsuario.java
│ │ ├── CorreoElectronico.java
│ │ └── Contrasena.java
│ ├── servicio/
│ │ └── ServicioDominioUsuario.java
│ ├── repositorio/
│ │ └── RepositorioUsuario.java
│ ├── evento/
│ │ ├── EventoUsuarioCreado.java
│ │ └── PublicadorEventosDominio.java
│ └── acl/
│ └── ServicioAutenticacionExterna.java
├── app/
│ └── src/main/java/com/ejemplo/app/
│ ├── ImplementacionAppUsuario.java
│ ├── ensamblador/
│ │ └── EnsambladorUsuario.java
│ └── mq/
│ └── ConsumidorMensajesUsuario.java
├── infrastructure/
│ └── src/main/java/com/ejemplo/infrastructure/
│ ├── persistencia/
│ │ ├── ImplementacionRepositorioUsuario.java
│ │ ├── mapeador/MapeadorUsuario.java
│ │ ├── po/ObjetoPersistenciaUsuario.java
│ │ └── convertidor/ConvertidorUsuario.java
│ └── mq/
│ └── ConfiguracionRocketMQ.java
├── acl/
│ └── src/main/java/com/ejemplo/acl/
│ ├── ImplementacionServicioAutExterna.java
│ ├── cliente/ClienteAutenticacion.java
│ ├── dto/
│ │ ├── SolicitudAutenticacion.java
│ │ ├── RespuestaAutenticacion.java
│ │ └── RespuestaDetalleUsuario.java
│ └── ensamblador/EnsambladorAutenticacion.java
└── bootstrap/
└── src/main/
├── java/com/ejemplo/bootstrap/
│ ├── Aplicacion.java
│ ├── config/
│ │ ├── ConfiguracionDominio.java
│ │ ├── ConfiguracionBaseDatos.java
│ │ ├── ConfiguracionWeb.java
│ │ └── ConfiguracionMQ.java
│ └── controlador/
│ └── ControladorUsuario.java
└── resources/
├── application.yml
└── logback-spring.xml
Diseño Detallado de Módulos
La descripción sigue la dirección de dependencias, desde las capas inferiores hasta las superiores.
1. Módulo API – Contratos Puros
Propósito: Definir interfaces de servicio expuestas externamente.
Restricciones: Sin lógica de implementación; sin dependencias a otros módulos; uso de cadenas en lugar de enumeraciones para evitar problemas de serialización.
// Respuesta.java – Envoltorio genérico de retorno
public class Respuesta<T> implements Serializable {
private boolean exito;
private String codigo;
private String mensaje;
private T datos;
// Getters y setters omitidos
}
// InterfazUsuario.java
public interface InterfazUsuario {
Respuesta<UsuarioDTO> crearUsuario(ComandoCrearUsuario comando);
Respuesta<UsuarioDTO> obtenerDetalleUsuario(String idUsuario);
Respuesta<Void> actualizarUsuario(ComandoActualizarUsuario comando);
}
// DTO / Comandos
public class UsuarioDTO implements Serializable {
private String idUsuario;
private String nombreUsuario;
private String correo;
private String estado; // Cadena en lugar de enum
// Getters y setters omitidos
}
public class ComandoCrearUsuario implements Serializable {
private String nombreUsuario;
private String correo;
private String contrasena;
// Getters y setters omitidos
}
public class ComandoActualizarUsuario implements Serializable {
private String idUsuario;
private String nombreUsuario;
private String correo;
// Getters y setters omitidos
}
2. Módulo Domain – Núcleo de Negocio
Propósito: Contener todas las reglas de negocio, siendo el corazón de la aplicación.
Restricciones: Sin dependencias externas; testeable con JVM puro.
// Agregado raíz
public class Usuario {
private IdentificadorUsuario id;
private NombreUsuario nombre;
private CorreoElectronico correo;
private Contrasena contrasena;
private EstadoUsuario estado;
private LocalDateTime fechaCreacion;
public Usuario(IdentificadorUsuario id, NombreUsuario nombre, CorreoElectronico correo,
Contrasena contrasena, EstadoUsuario estado, LocalDateTime fechaCreacion) {
this.id = id;
this.nombre = nombre;
this.correo = correo;
this.contrasena = contrasena;
this.estado = estado;
this.fechaCreacion = fechaCreacion;
}
public void cambiarContrasena(String contrasenaActual, String contrasenaNueva) {
if (!this.contrasena.coincideCon(contrasenaActual)) {
throw new ExcepcionDominio("Contraseña actual incorrecta");
}
this.contrasena = Contrasena.crear(contrasenaNueva);
}
public void activar() {
if (this.estado != EstadoUsuario.INACTIVO) {
throw new ExcepcionDominio("Solo usuarios inactivos pueden activarse");
}
this.estado = EstadoUsuario.ACTIVO;
}
// Getters omitidos
}
// Servicio de dominio
public class ServicioDominioUsuario {
private final RepositorioUsuario repositorio;
public ServicioDominioUsuario(RepositorioUsuario repositorio) {
this.repositorio = repositorio;
}
public Usuario crearUsuario(String nombre, String correo, String contrasena) {
if (repositorio.existePorNombre(nombre)) {
throw new ExcepcionDominio("Nombre de usuario ya existente");
}
if (repositorio.existePorCorreo(correo)) {
throw new ExcepcionDominio("Correo electrónico ya registrado");
}
return new Usuario(
IdentificadorUsuario.nuevoId(),
NombreUsuario.de(nombre),
CorreoElectronico.de(correo),
Contrasena.crear(contrasena),
EstadoUsuario.INACTIVO,
LocalDateTime.now()
);
}
}
// Interfaz de repositorio (implementada en infrastructure)
public interface RepositorioUsuario {
Usuario buscarPorId(IdentificadorUsuario id);
void guardar(Usuario usuario);
boolean existePorNombre(String nombre);
boolean existePorCorreo(String correo);
}
// Abstracción de anticorrupción (implementada en acl)
public interface ServicioAutenticacionExterna {
boolean validarToken(String token);
InfoUsuario obtenerInfoUsuario(String token);
}
3. Módulo APP – Servicios de Aplicación
Propósito: Implementar interfaces de api, manejar validaciones, transiciones y orquestación.
Restricciones: Sin reglas de negocio directas; depende solo de domain y api.
// ImplementacionAppUsuario.java
@Service
public class ImplementacionAppUsuario implements InterfazUsuario {
private final ServicioDominioUsuario servicioDominio;
private final RepositorioUsuario repositorio;
private final EnsambladorUsuario ensamblador;
private final PublicadorEventosDominio publicadorEventos;
public ImplementacionAppUsuario(ServicioDominioUsuario servicioDominio,
RepositorioUsuario repositorio,
EnsambladorUsuario ensamblador,
PublicadorEventosDominio publicadorEventos) {
this.servicioDominio = servicioDominio;
this.repositorio = repositorio;
this.ensamblador = ensamblador;
this.publicadorEventos = publicadorEventos;
}
@Override
@Transactional(rollbackFor = Exception.class)
public Respuesta<UsuarioDTO> crearUsuario(ComandoCrearUsuario comando) {
// 1. Validación de parámetros
Validador.validar(comando);
// 2. Crear agregado a través del servicio de dominio
Usuario usuario = servicioDominio.crearUsuario(
comando.getNombreUsuario(),
comando.getCorreo(),
comando.getContrasena()
);
// 3. Persistir el agregado
repositorio.guardar(usuario);
// 4. Publicar evento de dominio
publicadorEventos.publicar(new EventoUsuarioCreado(usuario.getId()));
// 5. Convertir y devolver DTO
return Respuesta.ok(ensamblador.aDTO(usuario));
}
}
4. Módulo Infrastructure – Integración Técnica
Propósito: Implementar interfaces de repositorio para bases de datos, cachés y mensajería.
Restricciones: Sin lógica de negocio; los objetos de persistencia no se filtran al dominio.
// ImplementacionRepositorioUsuario.java
@Repository
public class ImplementacionRepositorioUsuario implements RepositorioUsuario {
private final MapeadorUsuario mapeador;
private final ConvertidorUsuario convertidor;
public ImplementacionRepositorioUsuario(MapeadorUsuario mapeador, ConvertidorUsuario convertidor) {
this.mapeador = mapeador;
this.convertidor = convertidor;
}
@Override
public Usuario buscarPorId(IdentificadorUsuario id) {
ObjetoPersistenciaUsuario po = mapeador.seleccionarPorId(id.getValor());
return convertidor.aDominio(po);
}
@Override
public void guardar(Usuario usuario) {
ObjetoPersistenciaUsuario po = convertidor.aPO(usuario);
if (mapeador.existePorId(usuario.getId().getValor())) {
mapeador.actualizarPorId(po);
} else {
mapeador.insertar(po);
}
}
@Override
public boolean existePorNombre(String nombre) {
return mapeador.existePorNombre(nombre);
}
@Override
public boolean existePorCorreo(String correo) {
return mapeador.existePorCorreo(correo);
}
}
// Mapeador MyBatis
@Mapper
public interface MapeadorUsuario {
ObjetoPersistenciaUsuario seleccionarPorId(String id);
int insertar(ObjetoPersistenciaUsuario po);
int actualizarPorId(ObjetoPersistenciaUsuario po);
boolean existePorId(String id);
boolean existePorNombre(String nombre);
boolean existePorCorreo(String correo);
}
5. Módulo ACL – Capa de Aislamiento
Propósito: Implementar abstracciones de dominio para interacciones con sistemas externos, aislando protocolos y modelos.
Restricciones: Los DTOs externos no pasan al dominio directamente; se manejan excepciones de red como excepciones de negocio.
// ImplementacionServicioAutExterna.java
@Component
public class ImplementacionServicioAutExterna implements ServicioAutenticacionExterna {
private final ClienteAutenticacion cliente;
private final EnsambladorAutenticacion ensamblador;
public ImplementacionServicioAutExterna(ClienteAutenticacion cliente, EnsambladorAutenticacion ensamblador) {
this.cliente = cliente;
this.ensamblador = ensamblador;
}
@Override
public boolean validarToken(String token) {
try {
RespuestaAutenticacion respuesta = cliente.validarToken(token);
return respuesta != null && RespuestaAutenticacion.CODIGO_EXITO.equals(respuesta.getCodigo());
} catch (ExcepcionClienteRest e) {
throw new ExcepcionServicioExterno("Fallo en el servicio de autenticación", e);
}
}
@Override
public InfoUsuario obtenerInfoUsuario(String token) {
try {
RespuestaDetalleUsuario respuesta = cliente.obtenerDetalleUsuario(token);
return ensamblador.aInfoUsuario(respuesta);
} catch (ExcepcionClienteRest e) {
throw new ExcepcionServicioExterno("Fallo al obtener información del usuario", e);
}
}
}
// Cliente para servicio externo
@Component
public class ClienteAutenticacion {
private final PlantillaRest plantilla;
public ClienteAutenticacion(PlantillaRest plantilla) {
this.plantilla = plantilla;
}
public RespuestaAutenticacion validarToken(String token) {
return plantilla.postParaObjeto(
"http://servicio-auth/validar",
new SolicitudAutenticacion(token),
RespuestaAutenticacion.class
);
}
public RespuestaDetalleUsuario obtenerDetalleUsuario(String token) {
return plantilla.getParaObjeto(
"http://servicio-auth/usuario?token=" + token,
RespuestaDetalleUsuario.class
);
}
}
6. Módulo Bootstrap – Configuración y Arranque
Propósito: Punto de entrada de la aplicación, configuración de componentes técnicos.
Restricciones: Sin lógica de negocio.
// Aplicacion.java
@SpringBootApplication
@ComponentScan(basePackages = {
"com.ejemplo.bootstrap",
"com.ejemplo.app",
"com.ejemplo.infrastructure",
"com.ejemplo.acl"
// El paquete domain no se escanea; sus servicios se ensamblan explícitamente.
})
public class Aplicacion {
public static void main(String[] args) {
SpringApplication.run(Aplicacion.class, args);
}
}
// Ejemplo: Ensamblaje de servicio de dominio
@Configuration
public class ConfiguracionDominio {
@Bean
public ServicioDominioUsuario servicioDominioUsuario(RepositorioUsuario repositorio) {
return new ServicioDominioUsuario(repositorio);
}
}
// Ejemplo: Configuración de fuente de datos
@Configuration
public class ConfiguracionBaseDatos {
@Bean
@ConfigurationProperties("spring.datasource")
public DataSource fuenteDatos() {
return DataSourceBuilder.create().build();
}
@Bean
public PlatformTransactionManager gestorTransacciones(DataSource fuenteDatos) {
return new DataSourceTransactionManager(fuenteDatos);
}
}
Configuración Multi-Módulo con Maven
<!-- POM padre -->
<project>
<modules>
<module>api</module>
<module>domain</module>
<module>app</module>
<module>infrastructure</module>
<module>acl</module>
<module>bootstrap</module>
</modules>
</project>
<!-- Módulo API: sin dependencias internas -->
<project>
<artifactId>api</artifactId>
<dependencies/>
</project>
<!-- Módulo Domain: sin dependencias, tecnológicamente agnóstico -->
<project>
<artifactId>domain</artifactId>
<dependencies/>
</project>
<!-- Módulo APP -->
<project>
<artifactId>app</artifactId>
<dependencies>
<dependency>
<groupId>com.ejemplo</groupId>
<artifactId>api</artifactId>
</dependency>
<dependency>
<groupId>com.ejemplo</groupId>
<artifactId>domain</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</dependency>
</dependencies>
</project>
<!-- Módulo Infrastructure (ejemplo con MyBatis) -->
<project>
<artifactId>infrastructure</artifactId>
<dependencies>
<dependency>
<groupId>com.ejemplo</groupId>
<artifactId>domain</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
</dependencies>
</project>
<!-- Módulo ACL -->
<project>
<artifactId>acl</artifactId>
<dependencies>
<dependency>
<groupId>com.ejemplo</groupId>
<artifactId>domain</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
</dependencies>
</project>
<!-- Módulo Bootstrap -->
<project>
<artifactId>bootstrap</artifactId>
<dependencies>
<dependency><groupId>com.ejemplo</groupId><artifactId>api</artifactId></dependency>
<dependency><groupId>com.ejemplo</groupId><artifactId>app</artifactId></dependency>
<dependency><groupId>com.ejemplo</groupId><artifactId>domain</artifactId></dependency>
<dependency><groupId>com.ejemplo</groupId><artifactId>infrastructure</artifactId></dependency>
<dependency><groupId>com.ejemplo</groupId><artifactId>acl</artifactId></dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>
Beneficios del Enfoque
- Limites Claros: Las dependencias de compilación fuerzan la separación de capas.
- Alta Testeabilidad: El módulo domain es puro POJO, testeable sin contenedor Spring.
- Agnóstico Tecnológico: Cambios en ORM, MQ o RPC solo afectan a infrastructure y acl.
- Facilidad de Mantenimiento: Las reglas de negocio están centralizadas en domain, reduciendo el radio de cambio.
- Extensibilidad: Nuevas integraciones externas solo requieren nuevos módulos acl.