Catalog Service: Microservicio con Arquitectura Hexagonal
Microservicio empresarial de catálogo geográfico desarrollado con Arquitectura Hexagonal (Ports & Adapters), Java 25 y Spring Boot 4.0.5. Expone datos de referencia (países, comunidades autónomas, provincias, localidades y tipos de vía) mediante dos protocolos simultáneos: API REST y gRPC.
1. Código fuente
El repositorio completo está disponible en GitHub: iCesofT/arquitectura-hexagonal.
2. Introducción
catalog-service es un microservicio de catálogo geográfico que expone datos de referencia
mediante dos protocolos:
REST — API HTTP/1.1 y HTTP/2 generada a partir de especificaciones OpenAPI 3.0
gRPC — API binaria de alta eficiencia generada a partir de Protocol Buffers
El servicio está diseñado para entornos de producción de alta concurrencia, incorporando múltiples estrategias de optimización.
2.1. Objetivos de diseño
| Objetivo | Descripción |
|---|---|
Mantenibilidad | Código organizado en capas bien definidas mediante Arquitectura Hexagonal, con dependencias unidireccionales estrictamente controladas. |
Testeabilidad | Cada capa es testable de forma independiente. El dominio no tiene dependencia con ningún framework. |
Rendimiento | Caché de dos niveles (Caffeine + Redis), pool de conexiones HikariCP, virtual threads de Java y serialización optimizada con Jackson Blackbird. |
Observabilidad | Métricas con Micrometer/Prometheus, trazabilidad OTLP y actuator endpoints para health checks y readiness probes. |
Seguridad | Contenedor ejecutado con usuario no root, imagen Alpine minimalista, dependencias auditadas con OWASP Dependency-Check. |
Portabilidad | Docker multi-stage con capas optimizadas mediante Spring Boot Layertools, despliegue local con |
2.2. Stack tecnológico
| Categoría | Tecnología | Rol |
|---|---|---|
Runtime | Java 25 | Virtual threads, ZGC, rendimiento mejorado |
Framework | Spring Boot 4.0.5 | Contenedor de IoC, autoconfiguración, actuator |
Persistencia | Spring Data JPA + Hibernate 7 | ORM con soporte para batch DML y proxies de mejora |
Base de datos | PostgreSQL 16 | Base de datos relacional principal |
Migraciones | Liquibase 5 | Control de versiones del esquema de base de datos |
Caché L1 | Caffeine 3 | Caché local en memoria (heap), acceso sub-microsegundo |
Caché L2 | Redis 8 | Caché distribuida, compartida entre instancias |
API REST | OpenAPI 3.0 + Spring MVC | Generación de código a partir de contrato |
API gRPC | Protocol Buffers 4 + Spring gRPC | Comunicación binaria eficiente entre servicios |
Mapeo | MapStruct 1.6 | Generación de mappers en tiempo de compilación |
Logging | Log4j2 + LMAX Disruptor | Logging asíncrono de ultra-baja latencia |
Métricas | Micrometer + Prometheus | Instrumentación y exportación de métricas |
Contenedor | Docker + Alpine + ZGC | Imagen minimalista optimizada para producción |
3. Arquitectura Hexagonal
3.1. ¿Qué es?
La Arquitectura Hexagonal (también conocida como Ports & Adapters, propuesta por Alistair Cockburn en 2005) organiza el software en torno a la lógica de negocio, aislándola completamente de los detalles técnicos (base de datos, frameworks, protocolos de red, etc.) mediante el uso de puertos e interfaces de adaptación.
Permite que una aplicación sea controlada igualmente por usuarios, programas, tests automatizados o scripts batch, y que se desarrolle y pruebe en aislamiento de sus eventuales dispositivos de tiempo de ejecución y bases de datos.
3.2. Regla de dependencias
Las dependencias siempre apuntan hacia el interior del hexágono. Nunca desde el dominio hacia la infraestructura.
┌─────────────────────────────────────────────┐
│ INFRAESTRUCTURA │
│ ┌─────────────────────────────────────┐ │
│ │ APLICACIÓN │ │
│ │ ┌─────────────────────────────┐ │ │
│ │ │ DOMINIO │ │ │
│ │ │ (núcleo, sin framework) │ │ │
│ │ └─────────────────────────────┘ │ │
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
Las flechas de dependencia apuntan siempre hacia dentro:
Infraestructura → Aplicación → Dominio3.3. Puertos y adaptadores
| Concepto | Descripción | Ejemplo en catalog-service |
|---|---|---|
Puerto de entrada | Interfaz que define lo que la aplicación puede hacer (casos de uso). |
|
Puerto de salida | Interfaz que define lo que la aplicación necesita del exterior (repositorios). |
|
Adaptador primario | Implementación que conduce a la aplicación desde el exterior (REST, gRPC, CLI). |
|
Adaptador secundario | Implementación que conecta la aplicación al exterior (BD, caché, mensajería). |
|
3.4. Estructura de capas
catalog-service/
├── catalog-domain/ ← DOMINIO: entidades, puertos, excepciones
│ └── (sin dependencias de framework)
├── catalog-application/ ← APLICACIÓN: casos de uso, mediator
│ └── (depende sólo de catalog-domain)
├── catalog-infrastructure/ ← INFRAESTRUCTURA: adaptadores técnicos
│ ├── catalog-infrastructure-api-rest/ (adaptador REST entrante)
│ ├── catalog-infrastructure-api-grpc/ (adaptador gRPC entrante)
│ └── catalog-infrastructure-persistence-jpa/ (adaptador BD saliente)
└── catalog-bootstrap/ ← BOOTSTRAP: arranque, configuración Spring Boot
└── (ensambla todos los módulos)3.5. Flujo de una petición GET /paises/{id}
Cliente HTTP
│
▼
PaisesApiDelegateImpl (Adaptador REST)
│ GET /paises/{id}
▼
GetPaisByIdUseCase (Puerto de entrada)
│
▼
GetPaisByIdUseCaseImpl (Caso de uso)
│
▼
PaisRepositoryPort (Puerto de salida)
│
▼
PaisRepositoryAdapter (Adaptador de persistencia)
│
▼
JpaPaisRepository (Spring Data JPA)
│ SELECT * FROM paises WHERE id = ?
▼
PostgreSQL 16
│ País encontrado
▼ (respuesta sube por el mismo camino)
HTTP 200 OK + PaisDTO3.6. Beneficios obtenidos
| Beneficio | Evidencia en el proyecto |
|---|---|
Testeabilidad | La capa de dominio y aplicación se prueban sin Spring ni base de datos (tests en |
Intercambiabilidad | Se puede cambiar PostgreSQL por CockroachDB sin modificar dominio ni aplicación (existe perfil |
Independencia de protocolo | El mismo caso de uso se sirve tanto por REST ( |
Evolución independiente | Cada módulo Maven puede evolucionar, desplegarse y testearse de forma independiente. |
Comprensión del código | La estructura de paquetes refleja el dominio de negocio, no la tecnología. |
3.7. Verificación arquitectural con ArchUnit
El proyecto incorpora ArchUnit (arch-unit-maven-plugin) para validar
automáticamente las reglas de dependencia en cada build:
// Las dependencias entre capas se validan en CI/CD:
// ✅ Dominio no depende de aplicación ni infraestructura
// ✅ Aplicación sólo depende de dominio
// ✅ Infraestructura puede depender de aplicación y dominio
// ✅ Bootstrap puede depender de todos los módulosEl plugin arch-unit-maven-plugin se ejecuta automáticamente en la fase verify
de Maven, impidiendo que se mergee cualquier código que rompa las restricciones de arquitectura. |
4. Módulos Maven
4.1. Estructura multi-módulo
catalog-service/ (POM padre)
├── catalog-domain/ (módulo de dominio)
├── catalog-application/ (módulo de aplicación)
├── catalog-infrastructure/ (módulo de infraestructura — padre)
│ ├── catalog-infrastructure-api-rest/
│ ├── catalog-infrastructure-api-grpc/
│ └── catalog-infrastructure-persistence-jpa/
├── catalog-bootstrap/ (módulo de arranque)
└── catalog-doc/ (módulo de documentación)4.2. Grafo de dependencias
┌────────────────┐
│ catalog-domain │ (sin dependencias externas)
└───────┬────────┘
│ ←depende
┌─────────────┴─────────────┐
│ │
┌────────┴─────────┐ ┌────────┴──────────────────┐
│catalog-application│ │ catalog-infrastructure │
└────────┬─────────┘ │ (api-rest, api-grpc, jpa)│
│ ←depende └────────┬──────────────────┘
└────────────┬──────────────┘
│ ←depende
┌────────┴────────┐
│catalog-bootstrap│
└─────────────────┘| La dirección de las dependencias garantiza que el dominio nunca conoce los detalles de la infraestructura. Si el dominio necesita algo del exterior (como acceder a la base de datos), lo define como una interfaz (puerto) y la infraestructura la implementa (adaptador), invirtiendo la dependencia. |
5. Capa de Dominio
5.1. Propósito
La capa de dominio es el núcleo del hexágono. Contiene exclusivamente lógica de negocio: entidades, reglas de validación, contratos de repositorio (puertos de salida) y contratos de casos de uso (puertos de entrada).
El módulo catalog-domain no tiene ninguna dependencia de framework.
No importa Spring, JPA, Jackson ni ninguna otra librería de infraestructura.
Esto garantiza que las reglas de negocio son estables, portables y fácilmente
testeables en aislamiento. |
5.2. Entidades de dominio
Todas las entidades están implementadas como Java records, lo que garantiza:
Inmutabilidad — Los registros son inherentemente inmutables, evitando estados inconsistentes.
Validación en construcción — El constructor compacto valida invariantes de dominio.
Código conciso — Reducción de boilerplate sin perder expresividad.
5.2.1. Jerarquía de entidades geográficas
Pais ← Entidad independiente
ComunidadAutonoma ← Entidad independiente
└── Provincia ← Hace referencia a ComunidadAutonoma
└── Localidad ← Hace referencia a Provincia
TipoVia ← Entidad independiente (catálogo)5.2.2. Entidad Pais
public record Pais(String id, String denominacion) {
public Pais {
Objects.requireNonNull(id, "El id del país no puede ser nulo");
Objects.requireNonNull(denominacion, "La denominación no puede ser nula");
if (id.isBlank())
throw new ValidationException(PaisErrorType.ID_REQUERIDO);
if (denominacion.isBlank())
throw new ValidationException(PaisErrorType.DENOMINACION_REQUERIDA);
}
}5.2.3. Modelo de paginación
// Parámetros de entrada de paginación
public record Paginacion(int page, int size) { }
// Resultado paginado genérico
public record Pagina<T>(
List<T> contenido,
long totalElementos,
int totalPaginas,
int numeroPagina,
int tamanio
) {
public boolean esUltimaPagina() { return numeroPagina >= totalPaginas - 1; }
public boolean esPrimeraPagina() { return numeroPagina == 0; }
}5.3. Puertos del dominio
5.3.1. Puertos de entrada (casos de uso)
// Puerto de entrada — define el contrato de un caso de uso
public interface GetPaisByIdUseCase {
Pais getPorId(String id);
}
public interface GetPaginatedPaisesUseCase {
Pagina<Pais> getPaginados(Paginacion paginacion);
}| Entidad | Puertos de entrada |
|---|---|
|
|
|
|
|
|
|
|
|
|
5.4. Excepciones de dominio
DomainException (base)
├── NotFoundException ← HTTP 404 en el adaptador REST
└── ValidationException ← HTTP 422 en el adaptador RESTCada tipo de error se define como una implementación de la interfaz ErrorType,
lo que permite que los adaptadores REST conviertan las excepciones de dominio
en respuestas RFC 9457 Problem Detail sin que el dominio sepa nada de HTTP.
6. Capa de Aplicación
6.1. Propósito
La capa de aplicación orquesta los casos de uso: recibe una petición, invoca los puertos de salida necesarios del dominio y devuelve un resultado. No contiene lógica de negocio; sólo coordina.
@Service
@Transactional(readOnly = true)
public class GetPaisByIdUseCaseImpl implements GetPaisByIdUseCase {
private final PaisRepositoryPort repository;
@Override
public Pais getPorId(String id) {
return repository.getPorId(id)
.orElseThrow(() -> new NotFoundException(PaisErrorType.NO_ENCONTRADO, id));
}
}6.2. Patrón Mediator
Para escenarios más complejos, el proyecto implementa el patrón Mediator (también llamado Dispatcher o Bus), que desacopla quien emite una petición de quien la procesa.
// Marcador para cualquier petición que puede ser enviada por el mediator
public interface Request<R> { }
// Handler que procesa un tipo específico de Request
public interface RequestHandler<Req extends Request<Res>, Res> {
Res handle(Req request);
}
// Interfaz principal del Mediator
public interface Mediator {
<R> R send(Request<R> request);
}SpringMediator usa el ApplicationContext de Spring para resolver
dinámicamente el handler correcto a partir del tipo de la petición.
6.3. Pipeline Behaviors
Los Pipeline Behaviors son middlewares que envuelven la ejecución de cualquier handler, aplicando lógica transversal de forma declarativa y reutilizable:
Request
│
▼
ValidationBehavior.handle() ← valida la petición con Bean Validation
│
▼
LoggingBehavior.handle() ← registra la petición y el tiempo de respuesta
│
▼
DomainEventsBehavior.handle() ← recoge y publica eventos de dominio
│
▼
RequestHandler.handle() ← lógica del caso de uso
│
▼ (resultado)6.4. Patrón CQRS en Localidades
// Query (objeto de petición)
public record GetLocalidadByIdQuery(String id) implements Request<Localidad> { }
// Handler (procesador de la query)
@Component
public class GetLocalidadByIdHandler
implements RequestHandler<GetLocalidadByIdQuery, Localidad> {
@Override
public Localidad handle(GetLocalidadByIdQuery query) {
return repository.getPorId(query.id())
.orElseThrow(() -> new NotFoundException(...));
}
}
// En el adaptador REST se usa el Mediator (no el UseCase directamente):
mediator.send(new GetLocalidadByIdQuery(id));7. Optimizaciones de Rendimiento
7.1. Visión general
| Capa | Optimización | Impacto esperado |
|---|---|---|
Serialización | Jackson Blackbird (bytecode) | 20-40% menos tiempo de serialización/deserialización |
Caché L1 | Caffeine (heap local) | Respuesta en microsegundos para datos calientes |
Caché L2 | Redis (distribuida) | Eliminación de queries a BD en entornos multi-instancia |
Conexiones BD | HikariCP con parámetros optimizados | Reutilización de conexiones, mínima latencia de adquisición |
Concurrencia | Virtual Threads (Java 25) | Throughput máximo con consumo mínimo de hilos del SO |
I/O HTTP | HTTP/2 + compresión Gzip | Reducción de tamaño de respuesta y cabeceras |
Logging | Log4j2 + LMAX Disruptor async | El logging no bloquea el hilo de petición |
7.2. Caché de dos niveles: Caffeine + Redis
La caché sigue una estrategia read-through en dos niveles:
Petición
│
▼
┌──────────────────────────────────────────┐
│ Caffeine Cache (L1) │
│ Heap JVM · hasta 10.000 entradas │
│ TTL: 5 minutos · Eviction: LFU │
│ Acceso: < 1 microsegundo │
└──────────────────────────────────────────┘
│ (miss L1)
▼
┌──────────────────────────────────────────┐
│ Redis Cache (L2) │
│ Distribuida · compartida entre pods │
│ TTL: 10 minutos · Serialización: JSON │
│ Acceso: < 1 milisegundo (red local) │
└──────────────────────────────────────────┘
│ (miss L2)
▼
┌──────────────────────────────────────────┐
│ PostgreSQL │
│ Fuente de verdad · Query con índice │
└──────────────────────────────────────────┘@Configuration
@EnableCaching
public class CacheConfiguration {
@Bean("caffeineCacheManager")
public CacheManager caffeineCacheManager(CaffeineSpec spec) {
return new CaffeineCacheManager() {{ setCaffeineSpec(spec); }};
}
@Bean("redisCacheManager")
public RedisCacheManager redisCacheManager(
RedisConnectionFactory factory, ObjectMapper objectMapper) {
var config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10))
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new GenericJackson2JsonRedisSerializer(objectMapper)));
return RedisCacheManager.builder(factory).cacheDefaults(config).build();
}
@Primary
@Bean
public CacheManager compositeCacheManager(
@Qualifier("caffeineCacheManager") CacheManager l1,
@Qualifier("redisCacheManager") CacheManager l2) {
var composite = new CompositeCacheManager(l1, l2);
composite.setFallbackToNoOpCache(true); // No falla si Redis no está
return composite;
}
}7.3. Virtual Threads (Project Loom)
Los Virtual Threads (Java 21+ GA, mejorados en Java 25) son hilos ligeros gestionados por la JVM, no por el sistema operativo.
| Característica | Hilos tradicionales (OS) | Virtual Threads (JVM) |
|---|---|---|
Memoria por hilo | ~1 MB (stack fijo) | ~1 KB (stack dinámico) |
Creación | Costosa (syscall) | Muy barata (nanosegundos) |
Bloqueo I/O | Bloquea el hilo del SO | Desmonta el virtual thread; el carrier thread sigue libre |
Throughput | Limitado por número de cores × 2 | Miles de peticiones concurrentes con pocos cores |
spring:
threads:
virtual:
enabled: true # Habilita virtual threads para Tomcat y tareas async7.4. Parámetros JVM para contenedor
ENV JAVA_OPTS="\
-XX:+UseContainerSupport \
-XX:InitialRAMPercentage=50.0 \
-XX:MaxRAMPercentage=75.0 \
-XX:+UseZGC \
-XX:+ZGenerational \
-XX:+UseStringDeduplication \
-XX:MaxMetaspaceSize=256m \
-XX:+ExitOnOutOfMemoryError"| Parámetro | Propósito |
|---|---|
| La JVM respeta los límites de cgroup del contenedor en lugar de detectar la RAM del host. |
| El heap máximo es el 75% de la RAM disponible. El 25% restante es para metaspace, stack de hilos y buffers nativos. |
| ZGC generacional: GC de ultra-baja latencia con pausas < 1 ms, mejora el throughput hasta un 30% frente a ZGC clásico. |
| Ante un OOM, la JVM termina inmediatamente. Kubernetes reiniciará el pod en lugar de dejarlo en estado corrupto. |
8. Contenedores y Despliegue
8.1. Dockerfile Multi-Stage
# =============================================
# ETAPA 1: BUILDER
# =============================================
FROM eclipse-temurin:25-jdk-alpine AS builder
WORKDIR /app
COPY target/*.jar app.jar
RUN java -Djarmode=layertools -jar app.jar extract
# =============================================
# ETAPA 2: RUNTIME
# =============================================
FROM eclipse-temurin:25-jre-alpine
# Usuario no root — seguridad en contenedor
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
WORKDIR /app
# Capas ordenadas de menor a mayor frecuencia de cambio
COPY --from=builder /app/dependencies/ ./
COPY --from=builder /app/spring-boot-loader/ ./
COPY --from=builder /app/snapshot-dependencies/ ./
COPY --from=builder /app/application/ ./
EXPOSE 8080
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS org.springframework.boot.loader.launch.JarLauncher"]En un ciclo de desarrollo típico sólo cambia la capa application/.
Docker reutiliza las tres capas inferiores del caché, reduciendo el tiempo
de build y el ancho de banda de push al registry de ~200 MB a ~5-10 MB. |
8.2. Docker Compose
El fichero docker-compose.yml levanta el stack completo para desarrollo local
con health checks en todos los servicios y depends_on condicional para
garantizar el orden de arranque.
services:
backend:
image: catalog-service:latest
ports:
- "8080:8080"
depends_on:
database:
condition: service_healthy
cache:
condition: service_healthy
deploy:
resources:
limits:
memory: 512m
cpus: "1.0"
database:
image: postgres:16-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready -U catalog -d catalog"]
cache:
image: redis:8.0-alpine
command: redis-server --maxmemory 128mb --maxmemory-policy allkeys-lru
healthcheck:
test: ["CMD", "redis-cli", "ping"]9. Estrategia de Testing
9.1. Pirámide de tests
┌─────────────────────┐
│ E2E / Contract │ ← Pocos, lentos, frágiles
│ (gRPC / OpenAPI) │
├─────────────────────┤
│ Integración / BDD │ ← Cucumber con Spring context
│ (Persistence + UC) │
├─────────────────────┤
│ Unitarios │ ← Muchos, rápidos, sin Spring
│ (Domain + App) │
└─────────────────────┘9.2. Tests de dominio (JUnit 5 puro)
class PaisTest {
@Test
void deberiaCrearPaisValido() {
var pais = new Pais("ESP", "España");
assertThat(pais.id()).isEqualTo("ESP");
assertThat(pais.denominacion()).isEqualTo("España");
}
@Test
void deberiaFallarSiIdEsNulo() {
assertThatThrownBy(() -> new Pais(null, "España"))
.isInstanceOf(ValidationException.class);
}
}9.3. Tests BDD con Cucumber
Feature: Obtener país por identificador
Background:
Given un repositorio de países con los siguientes datos:
| id | denominacion |
| ESP | España |
| FRA | Francia |
Scenario: Obtener un país existente
When se solicita el país con id "ESP"
Then se obtiene el país con denominación "España"
Scenario: País no encontrado lanza excepción
When se solicita el país con id "XXX"
Then se lanza una NotFoundException con el id "XXX"9.4. Tests de persistencia con Testcontainers
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Testcontainers
class PaisRepositoryAdapterTest {
@Container
static PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:16-alpine")
.withDatabaseName("catalog_test");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
}
@Test
void deberiaObtenerPaisPorId() {
var resultado = adapter.getPorId("ESP");
assertThat(resultado).isPresent();
assertThat(resultado.get().denominacion()).isEqualTo("España");
}
}9.5. Cobertura objetivo
| Módulo | Tipo de tests | Cobertura objetivo |
|---|---|---|
| JUnit 5 unitarios | ≥ 90% — todas las validaciones de entidades |
| JUnit 5 + Cucumber BDD | ≥ 85% — todos los casos de uso con escenarios happy/error |
| Spock + jqwik + Testcontainers | ≥ 80% — adaptadores con BD real |
|
| ≥ 75% — todos los endpoints y manejo de errores |
10. Buenas Prácticas
10.1. Principios SOLID
10.1.1. S — Single Responsibility Principle
| Clase | Responsabilidad única |
|---|---|
| Mapear HTTP request/response a/desde el dominio |
| Orquestar la lógica de obtención de un país |
| Traducir entre el modelo JPA y el modelo de dominio |
| Traducir excepciones de dominio a respuestas HTTP |
10.1.2. O — Open/Closed Principle
// Para añadir tracing a TODOS los handlers:
// → Crear un nuevo PipelineBehavior (extensión)
// → No modificar ningún handler existente (cerrado)
@Component
@Order(1)
public class TracingBehavior<Req, Res> implements PipelineBehavior<Req, Res> {
@Override
public Res handle(Req request, Supplier<Res> next) {
return Tracer.trace(request.getClass().getSimpleName(), next);
}
}10.1.3. I — Interface Segregation Principle
// ✅ Correcto — Interfaces pequeñas y específicas
interface GetPaisByIdUseCase { Pais getPorId(String id); }
interface GetPaginatedPaisesUseCase { Pagina<Pais> getPaginados(Paginacion p); }
// ❌ Incorrecto — Una interfaz "dios" que obliga a implementar todo
interface PaisUseCase {
Pais getPorId(String id);
Pagina<Pais> getPaginados(Paginacion p);
void eliminar(String id); // ← fuerza dependencia innecesaria
}10.1.4. D — Dependency Inversion Principle
// catalog-application depende de la INTERFAZ (dominio)
private final PaisRepositoryPort repository; // interfaz en catalog-domain
// catalog-infrastructure-persistence-jpa IMPLEMENTA la interfaz
@Repository
public class PaisRepositoryAdapter implements PaisRepositoryPort { ... }
// Spring Boot inyecta la implementación concreta en tiempo de ejecución
// → El código de aplicación nunca referencia JPA ni Hibernate directamente10.2. Observabilidad con Actuator + Micrometer
| Endpoint | Propósito |
|---|---|
| Estado global: BD, Redis, disco. Usado por load balancer. |
| Liveness probe de Kubernetes (¿está vivo el proceso?). |
| Readiness probe de Kubernetes (¿está listo para recibir tráfico?). |
| Métricas en formato Prometheus para scraping. |
| Estado de los cachés Caffeine y Redis. |
| Cambio dinámico de nivel de logging sin reiniciar. |
10.3. Resumen de buenas prácticas
| Categoría | Práctica | Aplicación |
|---|---|---|
Arquitectura | Hexagonal (Ports & Adapters) | Separación estricta en capas, dependencias hacia el interior |
Arquitectura | Design-First API | OpenAPI y Protobuf como contrato canónico |
Código | SOLID | Interfaces mínimas, inversión de dependencias, clases con una responsabilidad |
Código | Inmutabilidad | Java records para entidades de dominio y DTOs internos |
Código | Fail Fast | Validación en constructores de entidades, no en servicios |
DDD | Lenguaje Ubicuo | Nombres en español que reflejan el negocio |
Rendimiento | Caché multinivel | Caffeine (L1) + Redis (L2) con fallback graceful |
Rendimiento | Virtual Threads | Máximo throughput en operaciones I/O bound |
Seguridad | Usuario no root en contenedor | Limitación de impacto ante vulnerabilidades RCE |
Seguridad | OWASP Dependency-Check | Detección automática de CVEs en dependencias |
Calidad | ArchUnit | Validación automática de reglas de arquitectura en CI |
Calidad | Spotless + SpotBugs | Formateo automático y análisis estático en cada build |
Observabilidad | Actuator + Micrometer + Prometheus | Visibilidad completa del estado del servicio en producción |
11. Inicio Rápido
11.2. Clonar y compilar
git clone https://github.com/iCesofT/arquitectura-hexagonal.git
cd arquitectura-hexagonal/catalog-service
./mvnw clean compile11.4. Verificar funcionamiento
# Health Check
curl http://localhost:8080/actuator/health
# API REST — Países
curl http://localhost:8080/api/v1/paises
# Swagger UI
open http://localhost:8080/openapi/ui.html11.5. API Endpoints
11.5.1. REST (Puerto 8080)
GET /api/v1/paises— Lista paginada de paísesGET /api/v1/paises/{id}— Obtener país por IDGET /api/v1/comunidades-autonomas— Lista de comunidades autónomasGET /api/v1/provincias— Lista de provinciasGET /api/v1/localidades— Lista de localidadesGET /actuator/health— Health checkGET /openapi/ui.html— Documentación interactiva
12. Autor y enlaces
Repositorio: github.com/iCesofT/arquitectura-hexagonal
Blog: www.icesoft.blog