Como definir un puerto de salida

Un puerto de salida es la interfaz que el servicio usa para hablar con el mundo afuera del dominio (base de datos, otros servicios). La interfaz vive en port/out/; el adapter que la implementa, en adapter/out/. El servicio depende solo de la interfaz.

Patrón en dos archivos

Para cada lado outbound del aggregate hay dos archivos:

Archivo Rol

{domainName}/port/out/{Aggregate}PersistencePort.java

Interfaz pública. Define la operación en términos del dominio.

{domainName}/adapter/out/persistence/{Aggregate}PersistenceAdapter.java

Implementación package-private. Traduce la operación a tecnología (JPA, HTTP, etc.).

Separación lectura / escritura

Los ports outbound se dividen por tipo de operación:

  • PersistencePort para escrituras (save, delete).

  • RetrievalPort para lecturas (findById, queries paginadas).

El mismo adapter puede implementar ambos — declara una sola dependencia al Repository. La separación de interfaces evita que un caso de uso de lectura termine con acceso transitivo a save(…​).

Ejemplo: port JPA para escritura

Interfaz del port:

public interface TaskPersistencePort {
    void save(Task task);
}

Repository (Spring Data, package-private):

interface TaskRepository extends JpaRepository<Task, String> {
}

Adapter (implementación, package-private):

@Component
@RequiredArgsConstructor
class TaskPersistenceAdapter implements TaskPersistencePort {

    private final TaskRepository repository;

    @Override
    public void save(Task task) {
        repository.save(task);
    }
}

Task es directamente una entidad JPA, heredada de AlphaIdEntity u otra clase base. No hay capa de mapeo entre dominio y modelo de persistencia. Para el detalle de las clases base ver Clases base JPA.

Ejemplo: port de lectura con paginación

Interfaz:

public interface TaskRetrievalPort {
    Optional<Task> findById(String id);
    Page<Task> findAll(Pageable pageable);
}

El mismo adapter, sumando la implementación:

@Component
@RequiredArgsConstructor
class TaskPersistenceAdapter implements TaskPersistencePort, TaskRetrievalPort {

    private final TaskRepository repository;

    @Override
    public void save(Task task) {
        repository.save(task);
    }

    @Override
    public Optional<Task> findById(String id) {
        return repository.findById(id);
    }

    @Override
    public Page<Task> findAll(Pageable pageable) {
        return repository.findAll(pageable);
    }
}

Pageable y Page de Spring Data se pasan tal cual al Repository. El controller envuelve Page<Task> en PageResponse — ver GET paginado en el how-to del controller.

El servicio depende del port, nunca del adapter

@Service
@RequiredArgsConstructor
class TaskService implements CreateTaskUseCase {

    private final TaskPersistencePort persistencePort;   (1)
    // private final TaskPersistenceAdapter adapter;     (2)
    // ...
}
1 Interfaz: el servicio depende del contrato.
2 Implementación concreta: rompe la inversión de dependencias y acopla el servicio a JPA.

Consecuencias prácticas:

  • El test del servicio reemplaza el port con @MockitoBean sin tocar JPA. Ver Como escribir tests de integración.

  • Cambiar el storage backend (JPA → in-memory, HTTP externo) requiere un adapter nuevo y cero cambios en el servicio.

Adaptadores HTTP a otros servicios

Cuando el port representa una llamada a otro microservicio, el patrón se mantiene: interfaz en port/out/, implementación en adapter/out/<algo> (por ejemplo adapter/out/http/). El adapter usa el cliente HTTP elegido (RestClient, WebClient) y traduce la respuesta al modelo de dominio. El arquetipo no incluye un cliente HTTP de referencia — la forma del contrato es la misma que el caso JPA.

Resumen

Pieza Visibilidad Ubicación

Interfaz del port

public

{domainName}/port/out/

Adapter

package-private

{domainName}/adapter/out/persistence/ (o adapter/out/http/)

Repository (si JPA)

package-private

Junto al adapter

Ver también