Introducción a DDD y arquitectura hexagonal con un ejemplo de aplicación en Java

Escrito por el , actualizado el .
planeta-codigo programacion
Enlace permanente Comentarios

La arquitectura de una aplicación define la estructura, organización y relación entre los componentes de la misma. En aplicaciones complejas utilizar DDD y arquitectura hexagonal son una opción recomendada. Hay varios libros técnicos dedicados a cada uno de ellos. En este artículo hago una introducción a DDD y arquitectura hexagonal y proporciono un ejemplo con el código fuente con el que implementar, analizar y ejecutar los conceptos teóricos en los que se basan.

Al desarrollar una aplicación hay que elegir una arquitectura de software. A lo largo del tiempo para las aplicaciones se han seguido diferentes tipos de arquitectura. Arquitectura spaghetti que es más bien la ausencia de arquitectura donde cualquier cosa se hace en cualquier lado, no se separan conceptos lo que da a lugar a problemas de mantenimiento y las aplicaciones son propensas a errores. La arquitectura por capas define varias capas cada una con una responsabilidad, la capa de presentación, la capa de lógica de negocio y la capa de persistencia, las capas superiores solo hacen uso de las capas inferiores.

Posteriormente, apareció el concepto de inyección de dependencias e inversión de control que se usa actualmente en la mayoría de los casos con independencia del lenguaje de programación, transformando la arquitectura por capas en la arquitectura hexagonal. Por otro lado, en aplicaciones complejas con mucha lógica de negocio se suele aplicar Domain Driven Design o DDD dividiendo la aplicación en bounded context y utilizando otros conceptos. La arquitectura hexagonal se complementa muy bien con DDD ya que sirve para independizar el dominio de los detalles externos de infraestructura tanto los que son usados para acceder al dominio como los usados por el dominio para acceder al exterior.

Domain-Driven Design

Domain-driven design o DDD es un concepto que propone que la estructura y lenguaje en el código del software (nombres de clases, métodos y variables) debería reflejar el dominio de negocio. Por ejemplo, en un software de eventos debería haber clases para el concepto de evento, recinto, tipos de entrada métodos como crear evento, cambiar fecha de evento, cancelar o posponer entre otros. DDD intenta evitar la existencia de modelos anémicos con poca lógica de negocio o conceptos exclusivamente técnicos. DDD define dos tipos de patrones, los estratégicos que hacen referencia a aspectos de negocio y los patrones tácticos más relacionados con detalles de implementación.

Patrones estratégicos:

  • Lenguaje ubicuo o ubiquitious language: son las definiciones de los conceptos aceptadas por todas las personas involucradas en un boundd context, personas tanto técnicas como de negocio. Son los expertos de negocio las que proporcionan el lenguaje ubicuo.
  • Subdominio: en las aplicaciones complejas estas se dividen en varias áreas funcionales. En una aplicación de comercio electrónico diferentes áreas son catálogo, inventario, compras, pagos, envíos o identidad.
  • Bounded Context: es el área de la aplicaicón en el que aplican el conjunto de definiciones del lenguaje ubicuo.
  • Context Map: define las relaciones entre los diferentes bounded context.

Patrones tácticos:

  • Entidad o entity: es la representación en el software de un objeto que se puede identificar de forma inequívoca del resto de instancias. Normalmente es una instancia de una clase con un identificativo asociado.
  • Value Object: es un objeto que no tiene identidad propia, como una fecha o importe.
  • Agregado o aggregate: es una entidad que mantiene las invariantes de un conjunto de entidades y value objects al hacerse modificaciones. El resto de agregados y entidades únicamente tiene una referencia al agregado pero no a las entidades agrupadas en él.
  • Servicio o service: contiene lógica de negocio sin estado con una función específica de dominio. Se utiliza un servicio cuando la lógica de negocio parece estar fuera de lugar en una entidad o value object.
  • Factorías: objetos cuya responsabilidad es crear objetos.
  • Repositorio o repository: abstrae a las entidades y agregados de los detalles de persistencia y búsqueda en las bases de datos.
  • Eventos de dominio o domain events: son notificaciones de que en el sistema ha sucedido algo, como que se ha creado un nuevo producto o se ha realizado una compra y que alguien está interesado en ser notificado.

Hay varios libros de referencia sobre DDD, dos de ellos son Domain-Driven Design: Tackling Complexity in the Heart of Software y Domain-Driven Design Distilled que explican más en detalle los patrones estratégicos. El libro Implementing Domain-Driven Design explica más en detalle los patrones tácticos.

Arquitectura hexagonal

La arquitectura hexagonal aísla e independiza al modelo de dominio de los elementos externos con el objetivo de que aunque los elementos externos cambien estos no afecten al modelo y de que se puedan hacer cambios en el dominio los elementos externos requieran los menores cambios necesarios.

La arquitectura hexagonal ubica los diferentes elementos de código en uno de los siguientes módulos:

  • Infraestructura: son los elementos externos con los que se comunica la aplicación, tanto de entrada como de salida. Puntos de entrada son una API con REST o GraphQL, mensajería con RabbitMQ o mediante línea de comandos. Puntos de salida son una base de datos relacional con PostgreSQL, no relacional con MongoDB, o también envío de mensajes con RabbitMQ. A los puntos de entrada se les denomina puertos y a los puntos de salida adaptadores.
  • Puertos: una aplicación puede ofrecer diferentes formas de comunicación al mismo tiempo, ya sea con una API REST o GraphQL, mensajes o mediante línea de comandos.
  • Adaptadores: igualmente una aplicación puede utilizar diferentes bases de datos o sistemas de comunicación con el exterior.
  • Aplicación: son los servicios que definen la API pública del dominio e independiza al dominio de cualesquiera elementos de infraestructura actuales o en el futuro.
  • Dominio: contiene la lógica de negocio de la aplicación. Esta puede ser implementada usando los principios de DDD.

En las relaciones de los elementos algo ubicado en infraestructura puede hacer uso de elementos de aplicación, los elementos de aplicación de elementos de dominio pero desde dominio no se permite hacer uso de elementos de infraestructura ni de aplicación, esto desacopla el dominio de los cambios que se produzcan en infraestructura, por ejemplo por añadir una base de datos diferente o añadir un nuevo puerto. La forma que tienen los elementos de dominio de hacer uso de elementos de infraestructura es haciendo uso de la inversión de dependencias, es decir, en vez de que dominio tenga dependencias de infraestructura consiguiendo que infraestructura dependa de dominio. Analizando los import de las clases Java es fácil reconocer cuando se está violando las reglas de dependencias, en caso de que dominio tenga un import de una clase de infraestructura o aplicación hay una dependencia que no debería permitirse. Hay herramientas para validar estas restricciones, hacer análisis estático de código con PMD.

Una gran ventaja de la arquitectura hexagonal es que los puertos y adaptadores no hace falta implementarlos al mismo tiempo que el dominio, el dominio solo ha de definir la interfaz que necesita y con posterioridad es posible realizar la implementación del adaptador en concreto que realice la persistencia. Esto permite retrasar la toma decisiones hasta tener más conocimiento de la solución más adecuada.

Diagrama de la arquitectura hexagonal

Diagrama de la arquitectura hexagonal
Fuente: Libro Implementing Domain-Driven Design

Ejemplo de aplicación Java con DDD y arquitectura hexagonal

En el siguiente ejemplo de implementación en Java de una aplicación con DDD y arquitectura hexagonal me baso en parte en los siguientes artículos relacionados o estos sirven como ejemplos de implementación de sus temas específicos.

El bus de comandos y consultas permiten implementar en la aplicación CQRS, utilizar una base de datos para las modificaciones y otra base de datos para las consultas, y asincronía. El bus de eventos permite implementar consistencia eventual en el caso de que la aplicación forme parte de otro conjunto de microservicios y actualizar la base de datos de consulta en CQRS.

La programación orientada a aspectos permite aplicar funcionalidades transversales de forma transparente a diferentes elementos, por ejemplo para añadir la transaccionalidad en una base de datos relacional.

En la aplicación de ejemplo con Spring Boot y Java se usa como puerto una API REST y como adaptador una interfaz que representaría base de datos relacional pero que en este caso para simplificar se implementa en un repositorio en memoria. Se utiliza Gradle como herramienta de construcción.

Estructura de directorios y paquetes

Esta es la estructura de directorios ubicando los artefactos de REST e implementación de repositorios en el paquete de infraestructura y otras dependencias e implementaciones del framework Spring. Los buses de comandos y consultas con las implementaciones de los casos de uso en el paquete de aplicación, estos llaman a los artefactos del dominio como repositorios y entidades. En infraestrucutra están los puertos y adaptadores dependientes de librerías y frameworks aislados del dominio como los elementos que componen la interfaz REST, varios elementos de Spring y las implementaciones de los repositorios que estarían acoplados a una base de datos relacional o no sql.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
$ tree src/
src/
├── main
│   ├── java
│   │   └── io
│   │       └── github
│   │           └── picodotdev
│   │               └── blogbitix
│   │                   └── dddhexagonal
│   │                       └── catalog
│   │                           ├── application
│   │                           │   ├── command
│   │                           │   │   ├── CreateEventCommandHandler.java
│   │                           │   │   └── CreateEventCommand.java
│   │                           │   ├── commandbus
│   │                           │   │   ├── CommandBus.java
│   │                           │   │   ├── CommandHandler.java
│   │                           │   │   └── Command.java
│   │                           │   ├── query
│   │                           │   │   ├── GetEventQueryHandler.java
│   │                           │   │   └── GetEventQuery.java
│   │                           │   ├── querybus
│   │                           │   │   ├── QueryBus.java
│   │                           │   │   ├── QueryHandler.java
│   │                           │   │   └── Query.java
│   │                           │   └── usecases
│   │                           │       ├── CreateEventUseCase.java
│   │                           │       └── GetEventUseCase.java
│   │                           ├── domain
│   │                           │   ├── model
│   │                           │   │   └── event
│   │                           │   │       ├── EventCancelledDomainEvent.java
│   │                           │   │       ├── EventCreatedDomainEvent.java
│   │                           │   │       ├── EventDate.java
│   │                           │   │       ├── EventDateRescheduledDomainEvent.java
│   │                           │   │       ├── EventId.java
│   │                           │   │       ├── Event.java
│   │                           │   │       ├── EventRepository.java
│   │                           │   │       ├── EventSchedule.java
│   │                           │   │       └── exceptions
│   │                           │   │           ├── EndDateIsBeforeStartDate.java
│   │                           │   │           └── InvalidDate.java
│   │                           │   └── shared
│   │                           │       └── domaineventbus
│   │                           │           ├── DomainEventBus.java
│   │                           │           ├── DomainEventCollection.java
│   │                           │           ├── DomainEventId.java
│   │                           │           └── DomainEvent.java
│   │                           ├── infrastructure
│   │                           │   ├── InMemoryEventRepository.java
│   │                           │   ├── rest
│   │                           │   │   ├── controllers
│   │                           │   │   │   └── EventController.java
│   │                           │   │   ├── converters
│   │                           │   │   │   ├── EventDateConverter.java
│   │                           │   │   │   └── EventIdConverter.java
│   │                           │   │   ├── exceptions
│   │                           │   │   │   ├── CustomRestExceptionHandler.java
│   │                           │   │   │   └── Error.java
│   │                           │   │   ├── serializer
│   │                           │   │   │   ├── EventDateSerializer.java
│   │                           │   │   │   ├── EventIdSerializer.java
│   │                           │   │   │   ├── EventScheduleSerializer.java
│   │                           │   │   │   └── EventSerializer.java
│   │                           │   │   └── spring
│   │                           │   │       └── SpringRestConfiguration.java
│   │                           │   └── spring
│   │                           │       ├── SpringCommandBus.java
│   │                           │       ├── SpringEventBus.java
│   │                           │       └── SpringQueryBus.java
│   │                           └── Main.java
│   └── resources
│       └── application.yml
└── test
    └── java

31 directories, 42 files
tree.sh

Entidades y value objects

En el dominio de catálogo hay eventos y habría varios value objects para representar ciertas propiedades del dominio.

Para la persistencia se puede hacer uso de Hibernate, una librerías ORM para Java, como alternativa o al mismo tiempo también es posible usar jOOQ. Las anotaciones de Hibernate requieren añadir imports que realmente son de infraestructura en las entidades de dominio, esta es una violación de la regla general de dependencias para evitarlo se puede realizar el mapeado con archivos hbm en formato XML, aunque por mayor sencillez se suele implementar con anotaciones. Como en este caso la persistencia solo se hace en memoria no están incluidas las anotaciones en la entidad.

Una entidad.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
package io.github.picodotdev.blogbitix.dddhexagonal.catalog.domain.model.event;

...

public class Event implements Serializable {

    private EventId id;
    private Status status;
    private EventSchedule schedule;

    private DomainEventCollection domainEvents;

    public enum Status {
        ACTIVE, CANCELLED, HIDDEN
    }

    public Event(EventId id, EventSchedule schedule) {
        this.id = id;
        this.status = Status.ACTIVE;
        this.schedule = schedule;
        this.domainEvents = new DomainEventCollection();
    }

    public EventId getId() {
        return id;
    }

    public Event.Status getStatus() {
        return status;
    }

    public EventSchedule getSchedule() {
        return schedule;
    }

    public DomainEventCollection getDomainEvents() {
        return domainEvents;
    }

    public static Event create(EventId id, EventSchedule schedule) throws Exception {
        if (!schedule.isFutureDate()) {
            throw new InvalidDate();
        }

        Event event = new Event(id, schedule);

        event.domainEvents.add(new EventCreatedDomainEvent(event.getId()));
        return event;
    }

    public void activate() {
        this.status = Status.ACTIVE;
        // TODO: domain event
    }

    public void cancel() {
        this.status = Status.CANCELLED;

        domainEvents.add(new EventCancelledDomainEvent(id));
    }

    public void hide() {
        this.status = Status.HIDDEN;
        // TODO: domain event
    }

    public void rescheduleDate(EventSchedule schedule) throws EndDateIsBeforeStartDate {
        this.schedule = schedule;

        domainEvents.add(new EventDateRescheduledDomainEvent(id));
    }
}
Event.java

Y varios value objects.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package io.github.picodotdev.blogbitix.dddhexagonal.catalog.domain.model.event;

...

public class EventId {

    private BigInteger id;

    protected EventId() {
    }

    protected EventId(BigInteger id) {
        this.id = id;
    }

    public BigInteger getValue() {
        return id;
    }

    public static EventId valueOf(String id) {
        return new EventId(new BigInteger(id));
    }

    public static EventId valueOf(BigInteger id) {
        return new EventId(id);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null) return false;
        if (!(o instanceof EventId)) return false;
        EventId that = (EventId) o;
        return Objects.equals(this.id, that.id);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id);
    }
}
EventId.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package io.github.picodotdev.blogbitix.dddhexagonal.catalog.domain.model.event;

...

public class EventDate {

    private final LocalDateTime datetime;

    private EventDate(final LocalDateTime datetime) throws InvalidDate {
        this.datetime = datetime;
    }

    public static EventDate valueOf(final String datetime) throws InvalidDate {
        return new EventDate(toDate(datetime));
    }

    public LocalDateTime geValue() {
        return datetime;
    }

    public boolean isAfter(final EventDate datetime) {
        return this.datetime.isAfter(datetime.getDateTime());
    }

    public boolean isFutureDate() {
        return datetime.isAfter(LocalDateTime.now());
    }

    private LocalDateTime getDateTime() {
        return datetime;
    }

    private static LocalDateTime toDate(final String date) throws InvalidDate {
        try {
            return LocalDateTime.parse(date);
        } catch (DateTimeParseException ex) {
            throw new InvalidDate();
        }
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null) return false;
        if (!(o instanceof EventDate)) return false;
        EventDate that = (EventDate) o;
        return Objects.equals(this.datetime, that.datetime);
    }

    @Override
    public int hashCode() {
        return Objects.hash(datetime);
    }
}
EventDate.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package io.github.picodotdev.blogbitix.dddhexagonal.catalog.domain.model.event;

...

public class EventSchedule {

    private EventDate startDate;
    private EventDate endDate;

    private EventSchedule(EventDate startDate, EventDate endDate) {
        this.startDate = startDate;
        this.endDate = endDate;
    }

    public EventDate getStartDate() {
        return startDate;
    }

    public EventDate getEndDate() {
        return endDate;
    }

    public boolean isFutureDate() {
        return startDate.isFutureDate();
    }

    public static EventSchedule valueOf(EventDate startDate, EventDate endDate) throws EndDateIsBeforeStartDate {
        validateStartBeforeEnd(startDate, endDate);
        return new EventSchedule(startDate, endDate);
    }

    private static void validateStartBeforeEnd(EventDate startDate, EventDate endDate) throws EndDateIsBeforeStartDate {
        if (startDate.isAfter(endDate)) {
            throw new EndDateIsBeforeStartDate();
        }
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null) return false;
        if (!(o instanceof EventSchedule)) return false;
        EventSchedule that = (EventSchedule) o;
        return Objects.equals(this.startDate, that.startDate)
                && Objects.equals(this.endDate, that.endDate);
    }

    @Override
    public int hashCode() {
        return Objects.hash(startDate, endDate);
    }
}
EventSchedule.java

Eventos de dominio

Cuando suceden cosas relevantes en el dominio se lanzan eventos de dominio para notificar a los interesados y que actúen en consecuencia. En este caso que un evento se ha creado, ha cambiado de fecha para notificar a los asistentes y realizar otra serie de acciones o ha sido cancelado que también desencadenará otras acciones.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package io.github.picodotdev.blogbitix.dddhexagonal.catalog.domain.model.event;

...

public class EventCreatedDomainEvent extends DomainEvent {

    private EventId eventId;

    public EventCreatedDomainEvent(EventId eventId) {
        this.eventId = eventId;
    }

    public EventId getEventId() {
        return eventId;
    }
}
EventCreatedDomainEvent.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package io.github.picodotdev.blogbitix.dddhexagonal.catalog.domain.model.event;

...

public class EventDateRescheduledDomainEvent extends DomainEvent {

    private EventId eventId;

    public EventDateRescheduledDomainEvent(EventId eventId) {
        this.eventId = eventId;
    }

    public EventId getEventId() {
        return eventId;
    }
}
EventDateRescheduledDomainEvent.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package io.github.picodotdev.blogbitix.dddhexagonal.catalog.domain.model.event;

...

public class EventCancelledDomainEvent extends DomainEvent {

    private EventId eventId;

    public EventCancelledDomainEvent(EventId eventId) {
        this.eventId = eventId;
    }

    public EventId getEventId() {
        return eventId;
    }
}
EventCancelledDomainEvent.java

El evento se registra en la entidad de dominio, es lanzado al bus de eventos por la clase de uso y sería manejado por sus interesados, en este caso la implementación de bus simplemente emite una traza en la terminal, una de sus implementaciones plausibles es hacer que los emita a un sistema de mensajes como RabbitMQ o implementarlo con el outbox pattern.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package io.github.picodotdev.blogbitix.dddhexagonal.catalog.infrastructure.spring;

...

@Component
@Primary
public class SpringEventBus implements DomainEventBus {

    public void publish(DomainEvent event) {
        System.out.printf("%s %s %s%n", event.getClass().getName(), event.getId().getValue(), event.getDate().format(DateTimeFormatter.ISO_DATE_TIME));
    }
}
SpringEventBus.java

Aplicación y casos de uso

La capa de aplicación es la interfaz que ofrece el dominio a sus consumidores, en esta implementación la interfaz se proporciona mediante un command bus y el query bus que hacen de intermediario entre la capa de aplicación y los casos de uso. Los casos de uso se abstraen de la utilización de los command bus y query bus y utilizan los servicios de dominio, entidades y value objects.

En esta implementación los comandos exponen los objetos de dominio, esto hace que el código que use esta capa de aplicación dependa de las entidades de dominio pero permite obtener la comprobación de tipos del compilador y hacer uso de las comprobaciones de los value objects. Otra forma de implementarlo hubiese sido que la clase comando únicamente tuviese propiedades de tipo primitivo lo que evita evitar el acoplamiento de código con las entidades de de dominio fuera del dominio pero por el contrario requiere añadir complejidad al tener que crear objetos DTO para devolver estos en vez de las entidades de dominio. Ambas implementaciones son válidas de forma general, cual aplicar depende según el caso o preferencia de que ventajas conservar.

Dos de las operaciones implementadas son obtener un evento y crear un evento, operaciones que son consumidas desde un puerto que a su vez las ofrece mediante una interfaz REST.

Los casos de uso de operaciones que realizan modificaciones en el modelo en el caso de utilizare una base de datos relacional son la ubicación para demarcar el ámbito de la transacción, con un aspecto que les aplique la anotación Transactional.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package io.github.picodotdev.blogbitix.dddhexagonal.catalog.application.usecases;

...

@Component
public class GetEventUseCase {

    private EventRepository eventRepository;

    public GetEventUseCase(EventRepository eventRepository) {
        this.eventRepository = eventRepository;
    }

    public Event handle(EventId id) {
        return eventRepository.findById(id);
    }
}
GetEventUseCase.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package io.github.picodotdev.blogbitix.dddhexagonal.catalog.application.usecases;

...

@Component
public class CreateEventUseCase {

    private EventRepository eventRepository;
    private DomainEventBus domainEventBus;

    public CreateEventUseCase(EventRepository eventRepository, DomainEventBus domainEventBus) {
        this.eventRepository = eventRepository;
        this.domainEventBus = domainEventBus;
    }

    public void handle(EventId id, EventSchedule schedule) throws Exception {
        Event event = Event.create(id, schedule);
        eventRepository.persist(event);

        domainEventBus.publish(event.getDomainEvents());
    }
}
CreateEventUseCase.java

Puertos y adaptadores

La interfaz REST es un puerto, punto de entrada o interfaz de la aplicación. Cuando se realiza una petición REST se construye y se envía al bus de consultas o comandos el objeto que representa la consulta o comando para el dominio.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package io.github.picodotdev.blogbitix.dddhexagonal.catalog.infrastructure.rest.controllers;

...

@RestController
@RequestMapping("/event")
public class EventController {

    private CommandBus commandBus;
    private QueryBus queryBus;

    public EventController(QueryBus queryBus, CommandBus commandBus) {
        this.queryBus = queryBus;
        this.commandBus = commandBus;
    }

    @PostMapping
    public ResponseEntity<String> createEvent(@RequestParam("startDate") EventDate startDate, @RequestParam("endDate") EventDate endDate) throws Exception {
        CreateEventCommand command = CreateEventCommand.Builder.getInstance()
                .eventSchedule(EventSchedule.valueOf(startDate, startDate))
                .build();
        commandBus.handle(command);
        return ResponseEntity.ok().build();
    }

    @GetMapping(path = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
    @ResponseBody
    public ResponseEntity<Event> getEvent(@PathVariable("id") EventId eventId) throws Exception {
        GetEventQuery command = GetEventQuery.Builder.getInstance()
                .eventId(eventId)
                .build();
        Event event = queryBus.handle(command);
        if (event == null) {
            return ResponseEntity.notFound().build();
        }
        return ResponseEntity.ok().body(event);
    }
}
EventController.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package io.github.picodotdev.blogbitix.dddhexagonal.catalog.application.query;

...

public class GetEventQuery extends Query<Event> {

    private EventId eventId;

    public GetEventQuery(EventId eventId) {
        this.eventId = eventId;
    }

    public EventId getEventId() {
        return eventId;
    }

    public static class Builder {

        private EventId eventId;

        public static GetEventQuery.Builder getInstance() {
            return new GetEventQuery.Builder();
        }

        public GetEventQuery.Builder eventId(EventId eventId) {
            this.eventId = eventId;
            return this;
        }

        public GetEventQuery build() {
            return new GetEventQuery(eventId);
        }
    }
}
GetEventQuery.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package io.github.picodotdev.blogbitix.dddhexagonal.catalog.application.command;

...

public class CreateEventCommand extends Command {

    private EventSchedule eventSchedule;

    private CreateEventCommand(EventSchedule eventSchedule) {
        this.eventSchedule = eventSchedule;
    }

    public EventSchedule getEventSchedule() {
        return eventSchedule;
    }

    public static class Builder {

        private EventSchedule eventSchedule;

        public static Builder getInstance() {
            return new Builder();
        }

        public Builder eventSchedule(EventSchedule eventSchedule) {
            this.eventSchedule = eventSchedule;
            return this;
        }

        public CreateEventCommand build() {
            return new CreateEventCommand(eventSchedule);
        }
    }
}
CreateEventCommand.java

El manejador del comando o consulta recibe los datos y lo delega al caso de uso.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package io.github.picodotdev.blogbitix.dddhexagonal.catalog.application.query;

...

@Component
public class GetEventQueryHandler implements QueryHandler<Event, GetEventQuery> {

    private GetEventUseCase useCase;

    public GetEventQueryHandler(GetEventUseCase useCase) {
        this.useCase = useCase;
    }

    @Override
    public Event handle(GetEventQuery query) throws Exception {
        return useCase.handle(query.getEventId());
    }
}
GetEventQueryHandler.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package io.github.picodotdev.blogbitix.dddhexagonal.catalog.application.command;

...

@Component
public class CreateEventCommandHandler implements CommandHandler<CreateEventCommand> {

    private EventRepository eventRepository;
    private CreateEventUseCase useCase;

    public CreateEventCommandHandler(EventRepository eventRepository, CreateEventUseCase useCase) {
        this.eventRepository = eventRepository;
        this.useCase = useCase;
    }

    @Override
    public void handle(CreateEventCommand command) throws Exception {
        System.out.println("Creando evento");

        EventId id = eventRepository.getId();
        useCase.handle(id, command.getEventSchedule());
    }
}
CreateEventCommandHandler.java

El repositorio es un adaptador, punto de salida o interfaz con un sistema externo, como es una base de datos. Para que el dominio no dependa de nada de infraestructura, el dominio crea una interfaz que se implementa en infraestructura, invirtiendo de esta forma la dependencia, con el objetivo deseado de que sea infraestructura quien dependa de dominio y no al revés.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package io.github.picodotdev.blogbitix.dddhexagonal.catalog.domain.model.event;

public interface EventRepository {

    EventId getId();

    Event findById(EventId id);

    void persist(Event event);
}
EventRepository.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package io.github.picodotdev.blogbitix.dddhexagonal.catalog.infrastructure;

...

@Component
public class InMemoryEventRepository implements EventRepository {

    private AtomicLong sequence;
    private Map<EventId, Event> events;

    public InMemoryEventRepository() {
        this.sequence = new AtomicLong();
        this.events = new HashMap<>();
    }

    @Override
    public EventId getId() {
        Long id = sequence.addAndGet(1);
        return EventId.valueOf(new BigInteger(id.toString()));
    }

    @Override
    public Event findById(EventId id) {
        return events.get(id);
    }

    @Override
    public void persist(Event event) {
        events.put(event.getId(), event);
    }
}
InMemoryEventRepository.java

Ejecución del ejemplo

Los endpoints para la interfaz REST, para la operación de modificación y para la operación de consulta son los siguientes.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
$ curl -v -X POST http://localhost:8080/event\?startDate\=2021-09-03T10:15:30\&endDate\=2021-12-03T22:00:00
*   Trying ::1:8080...
* Connected to localhost (::1) port 8080 (#0)
> POST /event?startDate=2021-09-03T10:15:30&endDate=2021-12-03T22:00:00 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.74.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 
< Content-Length: 0
< Date: Sun, 07 Feb 2021 00:49:29 GMT
< 
* Connection #0 to host localhost left intact

$ curl -v -X POST http://localhost:8080/event\?startDate\=aaa\&endDate\=bbb
*   Trying ::1:8080...
* Connected to localhost (::1) port 8080 (#0)
> POST /event?startDate=aaa&endDate=bbb HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.74.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 400 
< Content-Type: application/json
< Transfer-Encoding: chunked
< Date: Sun, 07 Feb 2021 00:51:01 GMT
< Connection: close
< 
{
  "status" : "BAD_REQUEST",
  "message" : "Invalid value aaa for startDate",
  "errors" : [ "Failed to convert value of type 'java.lang.String' to required type 'io.github.picodotdev.blogbitix.dddhexagonal.catalog.domain.model.event.EventDate'; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [@org.springframework.web.bind.annotation.RequestParam io.github.picodotdev.blogbitix.dddhexagonal.catalog.domain.model.event.EventDate] for value 'aaa'; nested exception is java.lang.IllegalArgumentException: io.github.picodotdev.blogbitix.dddhexagonal.catalog.domain.model.exceptions.InvalidDate" ]
* Closing connection 0

$ curl -X GET -H 'Accept: application/json' http://localhost:8080/event/1
{
  "id" : 1,
  "status" : "ACTIVE",
  "schedule" : {
    "startDate" : "2021-09-03T10:15:30",
    "endDate" : "2021-12-03T22:00:00"
  }
}
curl.sh

Este es el archivo de Gradle para la construcción y ejecución del ejemplo.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
plugins {
    id 'java'
    id 'application'
}

repositories {
    mavenCentral()
}

dependencies {
    implementation(platform("org.springframework.boot:spring-boot-dependencies:2.4.2"))

    implementation 'org.springframework.boot:spring-boot-starter'
    implementation 'org.springframework.boot:spring-boot-starter-web'

    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

test {
    useJUnitPlatform()
}

application {
    group = 'io.github.picodotdev.blogbitix'
    version = '0.0.1-SNAPSHOT'
    sourceCompatibility = '11'
    mainClassName = 'io.github.picodotdev.blogbitix.dddhexagonal.catalog.Main'
}
build.gradle
Terminal

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando siguiente comando:
./gradlew run


Comparte el artículo: