🔷 Guía Microservicios Spring Boot
Prácticas efectivas para microservicios que marcan diferencia en producción.
📋 Contexto
La mayoría de guías cubren lo básico. Esta guía se enfoca en prácticas que casi nadie configura bien pero que son críticas para producción.
🎯 Objetivo
Implementar microservicios robustos con:
- Plugins Kotlin correctamente configurados
- Entidades JPA optimizadas
- Mapeo DTO eficiente
- Seguridad con Kotlin DSL
- Testing con Testcontainers
📦 Alcance
Incluye:
- Configuración avanzada de plugins Kotlin
- Entidades sin
data class - Extension functions vs MapStruct
- Seguridad con Spring Security DSL
- Testing con Testcontainers
🔧 Configuración Base
Plugins Kotlin Esenciales
plugins {
kotlin("plugin.spring") version "2.2.20" // Hace @Component open
kotlin("plugin.jpa") version "2.2.20" // No-arg constructors
}
allOpen {
annotation("jakarta.persistence.Entity")
annotation("jakarta.persistence.MappedSuperclass")
annotation("jakarta.persistence.Embeddable")
}
¿Por qué importa?
- Sin
allOpen: Hibernate no puede crear proxies para lazy loading - Sin
plugin.jpa: Errores crípticos de instanciación en runtime
Dependencias Recomendadas
dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.flywaydb:flyway-core")
runtimeOnly("org.postgresql:postgresql")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.testcontainers:postgresql")
testImplementation("io.mockk:mockk:1.13.13")
}
🗄️ Entidades JPA
NO usar data class
Problemas:
equals()/hashCode()incluyen campos lazy →LazyInitializationExceptiontoString()puede causar recursión infinitacopy()rompe identidad del persistence context
Entidad Correcta
@Entity
@Table(name = "products")
class Product(
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null,
@Column(nullable = false)
var name: String,
@Column(nullable = false, precision = 10, scale = 2)
var price: BigDecimal,
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "category_id")
var category: Category? = null,
@Version
var version: Int? = null
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Product) return false
return id != null && id == other.id
}
override fun hashCode(): Int = javaClass.hashCode()
}
🔄 Mapeo DTOs
Extension Functions sobre MapStruct
// Entity → Response
fun Product.toResponse() = ProductResponse(
id = id!!,
name = name,
price = price,
categoryName = category?.name
)
// Request → Entity
fun CreateProductRequest.toEntity(category: Category?) = Product(
name = name,
price = price,
category = category
)
// PATCH parcial
fun Product.applyPatch(patch: PatchProductRequest): Product {
patch.name?.let { this.name = it }
patch.price?.let { this.price = it }
return this
}
Ventajas:
- Compilación directa sin annotation processors
- Refactoring seguro con IDE
- Kotlin compiler detecta cambios de campos
🔒 Seguridad (Kotlin DSL)
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
class SecurityConfig(private val jwtDecoder: JwtDecoder) {
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
csrf { disable() }
cors { }
sessionManagement {
sessionCreationPolicy = SessionCreationPolicy.STATELESS
}
authorizeHttpRequests {
authorize("/api/v1/auth/**", permitAll)
authorize("/actuator/health", permitAll)
authorize("/api/v1/admin/**", hasRole("ADMIN"))
authorize(anyRequest, authenticated)
}
oauth2ResourceServer {
jwt { jwtAuthenticationConverter = jwtAuthenticationConverter() }
}
headers {
contentSecurityPolicy {
policyDirectives = "default-src 'self'"
}
frameOptions { deny() }
}
}
return http.build()
}
}
🧪 Testing con Testcontainers
@TestConfiguration
class TestcontainersConfig {
@Bean
fun postgresqlContainer(): PostgreSQLContainer<*> {
return PostgreSQLContainer("postgres:16-alpine").apply {
withDatabaseName("testdb")
withUsername("test")
withPassword("test")
start()
}
}
}
@SpringBootTest
@ContextConfiguration(initializers = [TestcontainersConfig::class])
class ProductRepositoryTest {
@Autowired
lateinit var productRepository: ProductRepository
@Test
fun `should save and retrieve product`() {
val product = Product(name = "Test", price = BigDecimal("10.00"))
val saved = productRepository.save(product)
val found = productRepository.findById(saved.id!!)
assertThat(found).isPresent
assertThat(found.get().name).isEqualTo("Test")
}
}
✅ Checklist Producción
- Plugins Kotlin correctamente configurados
- Entidades sin
data class - Extension functions para mapeo
- Security DSL implementado
- Testcontainers para integración
- Flyway para migraciones
- Actuator endpoints expuestos
- Logs estructurados configurados