Saltar al contenido principal

🔷 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 → LazyInitializationException
  • toString() puede causar recursión infinita
  • copy() 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

📚 Referencias​