Estructura de carpetas, patrones de código, testing, comparación con otras arquitecturas y errores frecuentes
// El dominio define QUÉ necesita, no CÓMO se implementa package com.myapp.domain.port.out; public interface OrderRepository { Order save(Order order); Optional<Order> findById(OrderId id); List<Order> findByCustomer(CustomerId customerId); } // Nótese: sin @Repository, sin JPA, sin SQL. // Solo un contrato en Java puro.
// La infraestructura IMPLEMENTA el contrato del dominio package com.myapp.infrastructure.adapter.out; @Repository public class JpaOrderRepository implements OrderRepository { // ← implementa el port private final OrderJpaRepository jpaRepo; private final OrderMapper mapper; @Override public Order save(Order order) { OrderEntity entity = mapper.toEntity(order); return mapper.toDomain(jpaRepo.save(entity)); } }
@UseCase // annotation propia, no de Spring public class CreateOrderService implements CreateOrderUseCase { // Inyecta INTERFACES (ports), nunca implementaciones private final OrderRepository orderRepository; private final PaymentGateway paymentGateway; private final EmailNotifier emailNotifier; @Override public OrderId execute(CreateOrderCommand cmd) { Order order = Order.create(cmd.customerId(), cmd.items()); paymentGateway.charge(order.total()); orderRepository.save(order); emailNotifier.sendConfirmation(order); return order.id(); } }
@RestController @RequestMapping("/api/orders") public class RestOrderController { // Inyecta el USE CASE (input port), no el servicio directo private final CreateOrderUseCase createOrder; @PostMapping public ResponseEntity<OrderIdResponse> create(@RequestBody CreateOrderRequest req) { CreateOrderCommand cmd = mapper.toCommand(req); OrderId id = createOrder.execute(cmd); return ResponseEntity.ok(new OrderIdResponse(id)); } }