-
Notifications
You must be signed in to change notification settings - Fork 0
1. Arquitectura Recomendada
Para este curso adoptamos MVC organizado por features, también llamado Screaming Architecture. La estructura del proyecto "grita" el propósito de cada módulo — ves hotels/, users/, ping/, no adapters/, ports/ ni config/.
Empezamos con Arquitectura Hexagonal (Ports & Adapters) y la migramos a esta estructura más práctica. El razonamiento completo está en el Plan de Migración.
- Un paquete = una feature. Todo lo relacionado con una feature vive junto.
-
Máximo 2 niveles de profundidad dentro de una feature.
hotels/dto/es el máximo. -
Sin ports artificiales. El service es la capa de negocio. Spring ya sabe que un
@RestControlleres un adapter de entrada y un@Repositoryes un adapter de salida — no necesitamos declararlo explícitamente. -
Sin clases mapper separadas. La lógica de mapeo vive como
companion objecten el DTO de respuesta. -
Sin config por feature. Usa
@Service,@Repository,@Componentdirectamente. - YAGNI aplicado. No crees paquetes para features que aún no existen.
com.lgzarturo.springbootcourse/
│
├── SpringbootCourseApplication.kt
│
├── config/ ← Infraestructura transversal
│ ├── OpenApiConfig.kt ← Swagger/OpenAPI
│ └── WebConfig.kt ← CORS, interceptores globales
│
├── common/ ← Componentes reutilizables
│ ├── exception/
│ │ ├── ErrorResponse.kt ← DTO de error estandarizado (RFC 7807)
│ │ └── GlobalExceptionHandler.kt
│ ├── pagination/
│ │ ├── PageRequest.kt
│ │ ├── PageResult.kt
│ │ └── SortOrder.kt
│ ├── constants/
│ │ └── AppConstants.kt
│ └── extensions/
│ └── DateTimeExtensions.kt
│
└── features/ ← Features de negocio autocontenidas
├── hotels/
│ ├── HotelController.kt ← @RestController
│ ├── HotelService.kt ← @Service, lógica de negocio
│ ├── HotelRepository.kt ← @Repository, Spring Data JPA
│ ├── HotelEntity.kt ← @Entity JPA
│ ├── Hotel.kt ← Modelo de dominio puro
│ ├── HotelSearchCriteria.kt
│ └── dto/
│ ├── CreateHotelRequest.kt
│ ├── UpdateHotelRequest.kt
│ └── HotelResponse.kt ← Incluye companion object fromDomain()
│
├── users/
│ ├── UserController.kt
│ ├── UserService.kt
│ ├── UserRepository.kt
│ ├── User.kt
│ ├── UserRole.kt
│ ├── CreateUserCommand.kt
│ ├── exceptions/
│ │ └── DuplicateEmailException.kt
│ ├── valueobjects/
│ │ ├── Email.kt
│ │ └── UserId.kt
│ └── dto/
│ ├── UpdateUserRequest.kt
│ └── UserResponse.kt
│
├── ping/
│ ├── PingController.kt
│ ├── PingService.kt
│ └── dto/
│ └── PingResponse.kt
│
├── rooms/
└── sentry/
| Archivo | Responsabilidad | Annotations |
|---|---|---|
*Controller.kt |
Endpoints REST, valida input, delega al service |
@RestController, @RequestMapping
|
*Service.kt |
Lógica de negocio, orquesta repositorios | @Service |
*Repository.kt |
Acceso a datos, Spring Data JPA |
@Repository (interface que extiende JpaRepository) |
*Entity.kt |
Entidad JPA, mapeo a tabla |
@Entity, @Table
|
*.kt (modelo) |
Modelo de dominio puro, sin dependencias de Spring | data class simple |
dto/* |
DTOs de request/response con validaciones | data classes con anotaciones Jakarta |
valueobjects/ |
Value objects del dominio | @JvmInline value class |
exceptions/ |
Excepciones específicas de la feature | Exception subclasses |
En lugar de clases *Mapper.kt separadas, el DTO de respuesta expone un companion object:
data class HotelResponse(
val id: Long,
val name: String,
val city: String
) {
companion object {
fun fromDomain(hotel: Hotel) = HotelResponse(
id = requireNotNull(hotel.id),
name = hotel.name,
city = hotel.city
)
}
}- Legibilidad inmediata: un desarrollador nuevo entiende el propósito antes de leer código.
-
Máximo 2 niveles de anidación: elimina el laberinto de
adapters/rest/dto/request/. - Sin ceremonia sin valor: no hay interfaces con una sola implementación.
-
Testabilidad: el modelo de dominio (
Hotel.kt) sigue siendo un data class puro, testeable sin Spring. -
Escalabilidad: añadir una feature nueva es crear un directorio en
features/.
El modelo de dominio sigue siendo testeable sin contexto de Spring:
class PingServiceTest {
private val service = PingService()
@Test
fun `should return pong`() {
val result = service.getPing()
assertEquals("pong", result.message)
}
}| Patrón eliminado | Por qué | Reemplazo |
|---|---|---|
adapters/rest/ |
Spring ya sabe que un @RestController es un adapter |
Archivo directo en la feature |
application/ports/input/ |
Interfaces con una sola implementación no son ports | El service es la interfaz |
application/ports/output/ |
Idem | El repository es la interfaz |
config/HotelServiceConfig.kt |
Overkill para wiring simple |
@Service directamente |
HotelMapper.kt como @Component
|
Overkill para mapeos simples | companion object fromDomain() |
Stubs vacíos (cart/, payments/) |
YAGNI | Se crean cuando se necesiten |
© 2025 Spring Boot Course - API REST real
Desarrollado por lgzarturo
Licencia Creative Commons Attribution 4.0 International (CC BY 4.0) | Términos de Uso | Política de Privacidad
Recursos adicionales:
¿Tienes sugerencias o quieres reportar un error?
- Inicio
- Calendario de liberaciones
- Arquitectura
- Guía de Desarrollo
- Roadmap del Curso
- Testing y Calidad
- Seguridad y Flujo
- Gamificación
- Usa TDD para asegurar calidad.
- Mantén las capas desacopladas.
- Documenta tus APIs con Swagger.
- Aplica seguridad con JWT y roles.