Estructura del proyecto Spring Boot: Una guía práctica
Introducción
Cuando comienzas un proyecto en Spring Boot, una de las primeras decisiones críticas es cómo estructurarlo. Una organización adecuada del código puede ser la diferencia entre un proyecto fácil de mantener y uno que se convierta en un caos.
En este artículo, quiero compartir una estructura que he utilizado en varios proyectos con éxito. Aunque no existe una “estructura universal”, esta guía te ofrecerá un buen punto de partida que puedes adaptar según tus necesidades.
Estructura de proyecto
Recomiendo dividir el código en tres capas principales:
- application
- domain
- infrastructure
Capa de aplicación
Esta capa maneja los flujos de trabajo (workflows) de la aplicación. Utiliza el patrón Facade para encapsular la lógica de negocio y proporcionar una interfaz sencilla al resto de las capas.
Principales responsabilidades:
- Orquestar la lógica de negocio: La capa de aplicación interactúa con los servicios de dominio.
- Transformación de datos: Convierte DTOs en entidades y viceversa utilizando mappers dedicados.
- Gestión de excepciones: Maneja errores de manera centralizada y genera respuestas significativas para el cliente.
Ejemplo de estructura:
- application
- exceptions
- dto
- mappers
- workflows
- impl
Ejemplo de código:
@Component
public class OrderMapper {
public Order toEntity(CreateOrderRequest request) {
return new Order(request.getId(), request.getDescription(), request.getPrice());
}
public CreateOrderResponse toDto(Order order) {
return new CreateOrderResponse(order.getId(), order.getDescription(), order.getPrice());
}
}
@Slf4j
@Service
@RequiredArgsConstructor
public class CreateOrderWorkflowImpl implements CreateOrderWorkflow {
private final OrderMapper orderMapper;
private final OrderService orderService;
private final NotificationService notificationService;
private final AuditService auditService;
@Override
public void execute(CreateOrderRequest request) {
try {
Order order = orderMapper.toEntity(request);
orderService.create(order);
notificationService.send(new Notification("Order created"));
auditService.registry(INFO, "Order with id: " + order.getId() + " was created successfully");
} catch (OrderAlreadyExistsException e) {
log.warn("Order creation failed: {}", e.getMessage());
notificationService.send(new Notification("Error creating order"));
auditService.registry(ERROR, "Error creating order");
throw new OrderCreationException("Failed to create order", e);
}
}
}
Capa de dominio
La capa de dominio contiene la lógica de negocio central y debe mantenerse independiente de otras capas. Se organiza en:
- domain
- entities
- exceptions
- repositories
- services
Ejemplo de código:
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String description;
private BigDecimal price;
// Getters y setters
}
public interface OrderRepository extends JpaRepository<Order, Long> {
}
@Service
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
private final OrderRepository orderRepository;
@Override
public Order create(Order order) {
if (orderRepository.existsById(order.getId())) {
throw new OrderAlreadyExistsException("Order already exists with ID: " + order.getId());
}
return orderRepository.save(order);
}
}
Capa de infraestructura
La capa de infraestructura gestiona la interacción con el “mundo exterior”: bases de datos, APIs y la capa de presentación. Contiene controladores, repositorios y configuraciones.
Estructura sugerida:
- infrastructure
- adapters
- controllers
- configurations
Ejemplo de código:
@RestController
@RequestMapping("/orders")
@RequiredArgsConstructor
public class OrderController {
private final CreateOrderWorkflow createOrderWorkflow;
@PostMapping
public ResponseEntity<Void> createOrder(@Valid @RequestBody CreateOrderRequest request) {
createOrderWorkflow.execute(request);
return ResponseEntity.status(HttpStatus.CREATED).build();
}
}
Manejo global de excepciones
Centralizar el manejo de errores asegura respuestas consistentes:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(OrderCreationException.class)
public ResponseEntity<ErrorResponse> handleOrderCreationException(OrderCreationException ex) {
log.error("Error ID: {} - {}", ex.getErrorId(), ex.getMessage());
ErrorResponse errorResponse = ErrorResponse.builder()
.type("OrderCreationException")
.status(HttpStatus.BAD_REQUEST.value())
.message(ex.getMessage())
.build();
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
}
}
Últimos consejos
En las clases de excepciones, utiliza un identificador único para cada error. Esto facilita la búsqueda y el seguimiento de errores en los registros.
public class OrderAlreadyExistsException extends RuntimeException {
private final String errorId;
public OrderAlreadyExistsException(String message) {
super(message);
this.errorId = UUID.randomUUID().toString();
}
public String getErrorId() {
return errorId;
}
}
Conclusión
Esta estructura proporciona una base sólida para desarrollar aplicaciones Spring Boot escalables y mantenibles. Recuerda que cada proyecto es único, y esta guía debe adaptarse a tus necesidades específicas.
¿Y tú? ¿Cómo estructuras tus proyectos Spring Boot? ¡Comparte tus experiencias y opiniones en los comentarios!
¡Gracias por leer! 🚀