Skip to content

1. Arquitectura Recomendada

Arturo Lopez edited this page Apr 8, 2026 · 2 revisions

Arquitectura MVC por Features (Screaming Architecture)

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.


Principios de la Estructura

  1. Un paquete = una feature. Todo lo relacionado con una feature vive junto.
  2. Máximo 2 niveles de profundidad dentro de una feature. hotels/dto/ es el máximo.
  3. Sin ports artificiales. El service es la capa de negocio. Spring ya sabe que un @RestController es un adapter de entrada y un @Repository es un adapter de salida — no necesitamos declararlo explícitamente.
  4. Sin clases mapper separadas. La lógica de mapeo vive como companion object en el DTO de respuesta.
  5. Sin config por feature. Usa @Service, @Repository, @Component directamente.
  6. YAGNI aplicado. No crees paquetes para features que aún no existen.

Estructura del Proyecto

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/

Responsabilidades por Archivo

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

Mapeo sin Clases Mapper

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
        )
    }
}

Beneficios de Esta Estructura

  • 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/.

Ejemplo de Test de Dominio

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)
    }
}

Qué NO Hacer

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

Clone this wiki locally