Sesión 1 — El proyecto y el dominio¶
Rama: sesion-1
Lo que vas a lograr: el proyecto payments generado con el asistente de Spring, abierto en IntelliJ, con los paquetes hexagonales y el dominio modelado en Kotlin puro. Y de paso, entender el Kotlin que usamos.
Parte 1 — Generar el proyecto con Spring Initializr (15 min)¶
Spring Initializr es un asistente web que arma el esqueleto del proyecto. Entrá a https://start.spring.io y llená:
| Campo | Valor |
|---|---|
| Project | Gradle - Kotlin |
| Language | Kotlin |
| Spring Boot | 4.0.6 (o la 4.x estable más cercana) |
| Group | com.baqjug |
| Artifact | payments |
| Package name | com.baqjug.payments |
| Packaging | Jar |
| Java | 21 |
En Dependencies, agregá:
| Dependencia | Para qué |
|---|---|
| Spring Web | La API REST (adaptador de entrada) |
| Spring Data JPA | Guardar pagos (adaptador de salida) |
| PostgreSQL Driver | El driver de la base |
| Validation | Validar datos de entrada |
| Docker Compose Support | Que Spring levante la base con Docker al arrancar |
| OpenFeign | Llamar a la pasarela de pago (sesión 3) |
Hacé clic en GENERATE, y descomprimí dentro de tu repo:
Abrí payments en IntelliJ (File → Open). La primera vez baja el wrapper de Gradle y las dependencias. Esperá a que termine.
Parte 2 — Crear la estructura de paquetes (10 min)¶
En hexagonal se arranca por el centro: el dominio existe primero, antes que el controlador y la base. Creá esta estructura dentro de com.baqjug.payments (clic derecho → New → Package):
com.baqjug.payments
├── domain
│ └── model ← Payment, Money, PaymentId, PaymentStatus
├── application
│ ├── port
│ │ ├── in ← lo que ofrecemos (casos de uso)
│ │ └── out ← lo que necesitamos (pasarela, repositorio)
│ └── service ← implementa los casos de uso
└── infrastructure
└── adapter
├── in
│ └── web ← REST y, más adelante, Vaadin
└── out
├── persistence ← JPA
└── gateway ← Feign a la pasarela
El paquete in
in es palabra reservada en Kotlin. Como nombre de paquete funciona, pero el import va entre backticks: port.`in`. Si te molesta, podés usar incoming/outgoing.
Parte 3 — El dominio (15 min)¶
Negocio puro, sin una sola anotación de framework. Vamos clase por clase, explicando el Kotlin.
domain/model/PaymentId.kt:
package com.baqjug.payments.domain.model
import java.util.UUID
@JvmInline
value class PaymentId(val value: String = UUID.randomUUID().toString())
Kotlin al paso: value class
Un value class (con @JvmInline) envuelve un solo valor sin costo en tiempo de ejecución: en el bytecode es casi un String, pero en tu código es un tipo distinto. Sirve para no confundir un PaymentId con cualquier otro String. El = UUID.randomUUID().toString() es un valor por defecto: si no pasás un id, se genera uno.
domain/model/Money.kt:
package com.baqjug.payments.domain.model
import java.math.BigDecimal
data class Money(
val value: BigDecimal,
val currency: String = "COP"
) {
init {
require(value > BigDecimal.ZERO) { "El monto debe ser mayor a cero" }
}
}
Kotlin al paso: data class, val y require
Una data class te da equals, hashCode, toString y copy gratis: ideal para objetos de valor. val es inmutable (como final en Java); var sería mutable. El bloque init corre al construir el objeto, y require(...) lanza una excepción si la condición no se cumple: así la regla "el monto es positivo" vive en el dominio, no en un validador externo.
domain/model/PaymentStatus.kt:
domain/model/Payment.kt:
package com.baqjug.payments.domain.model
data class Payment(
val id: PaymentId = PaymentId(),
val amount: Money,
var status: PaymentStatus = PaymentStatus.PENDING
)
Kotlin al paso: var con intención
Casi todo es val, pero status es var a propósito: el pago nace PENDING y cambia a APPROVED o REJECTED. Es la única cosa que muta en el ciclo de vida del pago.
Lo importante: mirá lo que NO hay
Ni @Entity, ni @Component, ni un import de Spring o de JPA. Es Kotlin puro. Las reglas viven en el dominio. Esto se lee como negocio.
Cierre de la sesión¶
./gradlew compileKotlin
git add .
git commit -m "sesion-1: proyecto payments y dominio puro"
git branch sesion-1
En la Sesión 2 definimos los puertos, el servicio y un test que corre sin Spring.