Implementación de DDD con SpringBoot: Diseño de Capas y Módulos

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.

Etiquetas: DDD SpringBoot java Arquitectura de Microservicios patrones de diseño

Publicado el 7-14 19:27