Tutorial Spring Boot microservicios de calidad producción con Java 21, Spring Boot 3 y Spring Cloud
Bienvenido a este tutorial Spring Boot microservicios. Aprenderás a diseñar, construir y desplegar un sistema de microservicios listo para producción usando Java 21, Spring Boot 3, Spring Cloud y herramientas modernas como Spring Cloud Gateway, Eureka Service Discovery, JWT Spring Security, Docker Spring Boot, Kubernetes Spring Boot, Testcontainers Spring y OpenAPI springdoc.
1) Requisitos previos e instalación
- Java 21 (Temurin o Zulu)
- Maven o Gradle
- Docker y Docker Compose
- Git
- cURL o HTTPie
- Postman/Insomnia (opcional)
- kubectl y Kind/Minikube (opcional)
Comandos de verificación
1 2 3 4 5 6 |
java -version mvn -v # o gradle -v docker --version docker compose version kubectl version --client # opcional |
Explicación: Asegúrate de tener versiones recientes y compatibles. Java 21 es requerido para las capacidades de rendimiento y seguridad modernas.
Checklist:
- [ ] Java 21 instalado
- [ ] Maven/Gradle operativo
- [ ] Docker + Compose listos
- [ ] Git y cURL/HTTPie disponibles
- [ ] kubectl/Minikube (si vas a Kubernetes)
2) Arquitectura y módulos
Diagrama lógico:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
+-------------------+ +-------------------+ | config-server |<----->| config-repo (git)| +-------------------+ +-------------------+ | v +-------------------+ register +------------------+ | service-registry |<-------------->| api-gateway | | (Eureka) | +------------------+ ^ ^ | | | v | +------ +------------------+ Routes + Auth | | auth-service | (JWT validate) | +------------------+ | | | v | +------------------+ +------------------+ +----------| catalog-service |---| order-service | +------------------+ +------------------+ (PostgreSQL) (PostgreSQL) Optional: notification-service via events (Kafka/RabbitMQ) |
Patrón de comunicación:
- Sincrónico REST: OpenFeign entre servicios, resiliencia con Resilience4j.
- Mensajería opcional (Kafka/RabbitMQ) para eventos y desacoplo.
Checklist:
- [ ] Definiste módulos y responsabilidades
- [ ] Comunicación principal REST + resiliencia
- [ ] Consideraste eventos para notificaciones
3) Inicialización de proyectos
Usa Spring Initializr (web o CLI) por módulo. Dependencias clave:
- Spring Web, Actuator, Spring Security, Spring Data JPA, PostgreSQL Driver
- OpenFeign, Spring Cloud Config, Eureka Client, Spring Cloud Gateway
- Validation, Lombok, springdoc-openapi-starter-webmvc-ui
- Resilience4j, Micrometer, Prometheus
Estructura del repo (sugerida)
1 2 3 4 5 6 7 8 9 10 11 12 13 |
microservices-demo/ common-libs/ config-server/ service-registry/ api-gateway/ auth-service/ catalog-service/ order-service/ notification-service/ # opcional config-repo/ docker/ k8s/ |
Explicación: Cada módulo es un servicio independiente. common-libs agrupa DTOs y errores compartidos.
Ejemplo de pom.xml (catalog-service)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.acme</groupId> <artifactId>catalog-service</artifactId> <version>0.0.1-SNAPSHOT</version> <properties> <java.version>21</java.version> <spring.boot.version>3.3.2</spring.boot.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> <version>2.5.0</version> </dependency> <dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-spring-boot3</artifactId> </dependency> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-prometheus</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.flywaydb</groupId> <artifactId>flyway-core</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-resource-server</artifactId> </dependency> <dependency> <groupId>org.testcontainers</groupId> <artifactId>postgresql</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.testcontainers</groupId> <artifactId>junit-jupiter</artifactId> <scope>test</scope> </dependency> </dependencies> </project> |
Explicación: Incluimos JPA, OpenFeign, Eureka Client, springdoc-openapi, Resilience4j, Prometheus, Flyway y Resource Server para validar JWT.
Checklist:
- [ ] Módulos creados con Initializr
- [ ] Dependencias alineadas con objetivos
- [ ] Estructura de paquetes clara (com.acme.catalog, etc.)
4) Configuración centralizada (config-server)
application.yml (config-server)
1 2 3 4 5 6 7 8 9 10 11 12 13 |
server: port: 8888 spring: application: name: config-server cloud: config: server: git: uri: file:///${user.home}/projects/microservices-demo/config-repo default-label: main search-paths: . |
Explicación: Servidor Config lee propiedades desde un repo Git local o remoto.
bootstrap.yml en cada servicio
1 2 3 4 5 6 7 8 |
spring: application: name: catalog-service cloud: config: uri: http://localhost:8888 fail-fast: true |
Explicación: bootstrap.yml arranca temprano y obtiene configuración externa desde config-server.
config-repo/application-dev.yml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
eureka: client: serviceUrl: defaultZone: http://localhost:8761/eureka spring: datasource: url: jdbc:postgresql://localhost:5432/catalogdb username: catalog password: catalog jpa: hibernate: ddl-auto: validate properties: hibernate.format_sql: true hibernate.jdbc.time_zone: UTC management: endpoints.web.exposure.include: health,prometheus,info |
Explicación: Config común para dev. Puedes crear application-test.yml y application-prod.yml con credenciales seguras.
Checklist:
- [ ] config-server ejecuta y sirve propiedades
- [ ] Servicios tienen bootstrap.yml
- [ ] Perfiles dev/test/prod en config-repo
5) Service discovery y gateway
service-registry (Eureka) application.yml
1 2 3 4 5 6 7 8 9 10 |
server: port: 8761 spring: application: name: service-registry eureka: client: register-with-eureka: false fetch-registry: false |
Explicación: Modo servidor Eureka. Los clientes se registrarán aquí.
api-gateway application.yml (Spring Cloud Gateway)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
server: port: 8080 spring: application: name: api-gateway cloud: gateway: default-filters: - DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin - AddResponseHeader=X-Request-Id, "#{T(java.util.UUID).randomUUID()}" routes: - id: auth uri: lb://auth-service predicates: - Path=/auth/** - id: catalog uri: lb://catalog-service predicates: - Path=/api/catalog/** filters: - StripPrefix=1 - Retry=5,series=SERVER_ERROR,methods=GET,backoff=firstBackoff=100ms,maxBackoff=2s - id: order uri: lb://order-service predicates: - Path=/api/orders/** filters: - StripPrefix=1 globalcors: corsConfigurations: '[/**]': allowedOrigins: "*" allowedMethods: "GET,POST,PUT,DELETE" allowedHeaders: "*" maxAge: 3600 discovery: locator: enabled: true lower-case-service-id: true management: endpoints.web.exposure.include: health,prometheus |
Explicación: Rutas por servicio por nombre lógico vía Eureka (lb://). Filtros globales de CORS, logging y reintentos configurados.
Checklist:
- [ ] Eureka server en 8761
- [ ] Servicios registrados como clientes
- [ ] Gateway enruta y aplica CORS y reintentos
6) Seguridad y autenticación (auth-service, JWT RS256)
SecurityConfig.java (auth-service)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
@Configuration @EnableMethodSecurity public class SecurityConfig { @Bean SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { return http .csrf(csrf -> csrf.disable()) .authorizeHttpRequests(auth -> auth .requestMatchers("/auth/**", "/v3/api-docs/**", "/swagger-ui/**").permitAll() .anyRequest().authenticated()) .build(); } } |
Explicación: Endpoints públicos de autenticación y OpenAPI. Resto requiere autenticación.
JwtService.java (RS256)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
@Service public class JwtService { private final KeyPair keyPair; // cargado de keystore o PEMs public JwtService(@Value("${security.jwt.private-key-pem}") Resource priv, @Value("${security.jwt.public-key-pem}") Resource pub) throws Exception { this.keyPair = PemUtils.readRsaKeyPair(priv.getInputStream(), pub.getInputStream()); } public String generateAccessToken(String subject, Collection<String> roles) { Instant now = Instant.now(); return Jwts.builder() .subject(subject) .claim("roles", roles) .issuedAt(Date.from(now)) .expiration(Date.from(now.plusSeconds(900))) .signWith(keyPair.getPrivate(), Jwts.SIG.RS256) .compact(); } public String generateRefreshToken(String subject) { Instant now = Instant.now(); return Jwts.builder() .subject(subject) .issuedAt(Date.from(now)) .expiration(Date.from(now.plusSeconds(86400))) .signWith(keyPair.getPrivate(), Jwts.SIG.RS256) .compact(); } } |
Explicación: Genera tokens access (15 min) y refresh (24 h) firmados con RS256. Carga de claves PEM.
AuthController.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
@RestController @RequestMapping("/auth") @RequiredArgsConstructor public class AuthController { private final UserService userService; private final JwtService jwtService; private final PasswordEncoder encoder; @PostMapping("/register") public ResponseEntity<?> register(@Valid @RequestBody RegisterRequest req) { userService.createUser(req.email(), encoder.encode(req.password()), Set.of("ROLE_USER")); return ResponseEntity.status(HttpStatus.CREATED).build(); } @PostMapping("/login") public TokenResponse login(@RequestBody LoginRequest req) { var user = userService.authenticate(req.email(), req.password()); return new TokenResponse( jwtService.generateAccessToken(user.email(), user.roles()), jwtService.generateRefreshToken(user.email())); } @PostMapping("/refresh") public TokenResponse refresh(@RequestBody RefreshRequest req) { var subject = userService.validateRefreshToken(req.refreshToken()); return new TokenResponse(jwtService.generateAccessToken(subject, Set.of("ROLE_USER")), req.refreshToken()); } } |
Explicación: Registro, login y refresh. Al registrar, usamos BCrypt; al loguear, generamos ambos tokens.
Validación de JWT en gateway y servicios
1 2 3 4 5 6 7 8 |
# api-gateway application.yml (añadir) spring: security: oauth2: resourceserver: jwt: public-key-location: classpath:jwt/public.pem |
Explicación: Gateway y servicios actúan como Resource Servers validando tokens con clave pública RS256.
Buenas prácticas:
- BCrypt para contraseñas.
- Expiraciones cortas en access token.
- Rotación de claves y almacenamiento en Secret.
Checklist:
- [ ] Endpoints /auth funcionando
- [ ] Validación JWT en gateway y servicios
- [ ] BCrypt configurado
7) Persistencia y dominio (catalog y order)
Entidad Product (catalog-service)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
@Entity @Table(name = "products") @Data @Builder @NoArgsConstructor @AllArgsConstructor public class Product { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false) private String name; private String description; @Column(nullable = false) private BigDecimal price; @Column(nullable = false) private Integer stock; } |
Explicación: Modelo básico de producto con precio y stock.
Repositorio y servicio
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public interface ProductRepository extends JpaRepository<Product, Long>, JpaSpecificationExecutor<Product> {} @Service @RequiredArgsConstructor public class ProductService { private final ProductRepository repo; @Transactional(readOnly = true) public Page<Product> list(String q, Pageable pageable) { var spec = (Specification<Product>) (root, cq, cb) -> q == null ? null : cb.like(cb.lower(root.get("name")), "%"+q.toLowerCase()+"%"); return repo.findAll(spec, pageable); } } |
Explicación: Filtros dinámicos con Specification y paginación.
Controlador
1 2 3 4 5 6 7 8 9 10 11 12 |
@RestController @RequestMapping("/catalog/products") @RequiredArgsConstructor public class ProductController { private final ProductService service; @GetMapping public Page<Product> list(@RequestParam(required=false) String q, Pageable pageable) { return service.list(q, pageable); } } |
Explicación: Endpoint paginado y filtrado.
Flyway V1__init.sql
1 2 3 4 5 6 7 8 |
CREATE TABLE IF NOT EXISTS products ( id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, description TEXT, price NUMERIC(12,2) NOT NULL, stock INT NOT NULL ); |
Explicación: Migración inicial de catálogo.
Order y OrderItem (order-service)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
@Entity @Table(name="orders") @Data @NoArgsConstructor @AllArgsConstructor @Builder public class Order { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String userEmail; private Instant createdAt; @OneToMany(mappedBy="order", cascade=CascadeType.ALL, orphanRemoval=true) private List<OrderItem> items = new ArrayList<>(); } @Entity @Table(name="order_items") @Data @NoArgsConstructor @AllArgsConstructor @Builder public class OrderItem { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private Long productId; private Integer quantity; private BigDecimal unitPrice; @ManyToOne(fetch=FetchType.LAZY) @JoinColumn(name="order_id") private Order order; } |
Explicación: Relación 1-N con cascada para persistir ítems.
Servicio transaccional
1 2 3 4 5 6 7 8 9 10 11 12 |
@Service @RequiredArgsConstructor public class OrderService { private final OrderRepository repo; @Transactional public Order create(Order order) { order.setCreatedAt(Instant.now()); order.getItems().forEach(i -> i.setOrder(order)); return repo.save(order); } } |
Explicación: Transacción atómica para crear órdenes.
Checklist:
- [ ] Entidades y migraciones Flyway creadas
- [ ] Paginación y filtros
- [ ] Transacciones en casos de uso críticos
8) Comunicación entre servicios y resiliencia
Cliente OpenFeign (order consulta precios en catalog)
1 2 3 4 5 6 |
@FeignClient(name = "catalog-service", path = "/catalog/products", configuration = FeignConfig.class) public interface CatalogClient { @GetMapping("/{id}") ProductDto findById(@PathVariable Long id); } |
Explicación: Llama a catalog-service por service discovery.
Configuración Feign y Resilience4j
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
@Configuration public class FeignConfig { @Bean public Request.Options feignOptions() { return new Request.Options(1000, 2000); } } # application.yml resilience4j: circuitbreaker: instances: catalog: slidingWindowSize: 10 failureRateThreshold: 50 retry: instances: catalog: maxAttempts: 3 waitDuration: 200ms |
Explicación: Timeouts de Feign y circuit breaker + retry para resiliencia.
Checklist:
- [ ] OpenFeign configurado con timeouts
- [ ] Resilience4j con circuit breakers y retries
9) Manejo de errores y DTOs (common-libs)
ProblemDetails (RFC 7807)
1 2 3 4 5 6 7 8 9 10 |
@Data @Builder public class ApiError { private String type; private String title; private int status; private String detail; private String instance; private Instant timestamp; } |
Explicación: Estructura estándar de errores.
ControllerAdvice global
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
@RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<ApiError> handleValidation(MethodArgumentNotValidException ex, HttpServletRequest req){ ApiError error = ApiError.builder() .type("https://example.com/problems/validation") .title("Validation failed") .status(400) .detail(ex.getMessage()) .instance(req.getRequestURI()) .timestamp(Instant.now()) .build(); return ResponseEntity.badRequest().body(error); } } |
Explicación: Respuestas uniformes ante errores de validación.
Checklist:
- [ ] DTOs compartidos y errores estándar
- [ ] ControllerAdvice aplicado en servicios
10) Documentación: OpenAPI springdoc
1 2 3 4 5 6 7 |
# application.yml en cada servicio springdoc: api-docs: enabled: true swagger-ui: path: /swagger-ui.html |
Explicación: springdoc-openapi expone /v3/api-docs y Swagger UI por servicio. En el gateway, enlaza a los UIs de cada servicio.
Checklist:
- [ ] OpenAPI expuesto por servicio
- [ ] Enlaces accesibles desde el gateway
11) Observabilidad y salud
Actuator y Prometheus
1 2 3 4 5 |
management: endpoints.web.exposure.include: health,info,prometheus endpoint.health.probes.enabled: true metrics.tags.application: ${spring.application.name} |
Explicación: Activa endpoints de salud y métricas para Prometheus.
Opcional: OpenTelemetry (OTLP)
1 2 3 4 5 6 7 |
management: tracing: enabled: true otlp: tracing: endpoint: http://otel-collector:4317 |
Explicación: Exporta trazas a un colector OTLP (Jaeger/Tempo).
Checklist:
- [ ] /actuator/health y /actuator/prometheus disponibles
- [ ] Trazas si usas OpenTelemetry
12) Pruebas (JUnit 5, Mockito, Testcontainers Spring)
Testcontainers para repositorio
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
@Testcontainers @SpringBootTest class ProductRepositoryIT { @Container static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine"); @DynamicPropertySource static void props(DynamicPropertyRegistry r){ r.add("spring.datasource.url", postgres::getJdbcUrl); r.add("spring.datasource.username", postgres::getUsername); r.add("spring.datasource.password", postgres::getPassword); } @Autowired ProductRepository repo; @Test void savesAndFinds(){ var p = repo.save(Product.builder().name("A").price(new BigDecimal("10.00")).stock(5).build()); assertTrue(repo.findById(p.getId()).isPresent()); } } |
Explicación: Levanta PostgreSQL efímero y prueba persistencia real.
Prueba de controlador con SpringBootTest + WebTestClient
1 2 3 4 5 6 7 8 9 10 11 12 |
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class ProductControllerE2E { @Autowired WebTestClient web; @Test void listProducts(){ web.get().uri("/catalog/products") .exchange() .expectStatus().isOk(); } } |
Explicación: Prueba endpoint HTTP real.
Checklist:
- [ ] Testcontainers configurado
- [ ] Pruebas unitarias e integración
13) Contenedorización: Docker Spring Boot
Dockerfile multi-stage (catalog-service)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
# Stage 1: build FROM maven:3.9-eclipse-temurin-21 AS builder WORKDIR /app COPY pom.xml . RUN mvn -q -e -DskipTests dependency:go-offline COPY src ./src RUN mvn -q -DskipTests package # Stage 2: runtime FROM eclipse-temurin:21-jre ENV JAVA_OPTS="-XX:+UseG1GC -XX:MaxRAMPercentage=75" WORKDIR /opt/app COPY --from=builder /app/target/catalog-service-*.jar app.jar EXPOSE 8081 ENTRYPOINT ["sh","-c","java $JAVA_OPTS -jar app.jar"] |
Explicación: Construcción en una imagen y ejecución ligera con Temurin JRE.
.dockerignore
1 2 3 4 5 |
target/ .git .idea *.iml |
Explicación: Evita copiar artefactos innecesarios.
docker-compose.yml (servicios + PostgreSQL + Prometheus opcional)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
version: "3.9" services: config-server: build: ./config-server ports: ["8888:8888"] environment: - SPRING_PROFILES_ACTIVE=dev volumes: - ./config-repo:/config-repo eureka: build: ./service-registry ports: ["8761:8761"] depends_on: [config-server] postgres-catalog: image: postgres:16-alpine environment: POSTGRES_DB: catalogdb POSTGRES_USER: catalog POSTGRES_PASSWORD: catalog ports: ["5433:5432"] postgres-order: image: postgres:16-alpine environment: POSTGRES_DB: orderdb POSTGRES_USER: orders POSTGRES_PASSWORD: orders ports: ["5434:5432"] auth-service: build: ./auth-service depends_on: [eureka] catalog-service: build: ./catalog-service depends_on: [postgres-catalog, eureka] order-service: build: ./order-service depends_on: [postgres-order, eureka] api-gateway: build: ./api-gateway ports: ["8080:8080"] depends_on: [auth-service, catalog-service, order-service] |
Explicación: Orquesta microservicios, bases de datos y gateway. Ajusta puertos según tus yml.
Comandos:
1 2 3 |
docker compose build docker compose up -d |
Explicación: Construye e inicia el entorno completo en segundo plano.
Checklist:
- [ ] Imágenes construidas
- [ ] Servicios arrancan sin errores
- [ ] Healthchecks OK
14) Despliegue en Kubernetes (opcional pero recomendado)
ConfigMap y Secret (gateway)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
apiVersion: v1 kind: ConfigMap metadata: { name: api-gateway-config } data: application.yaml: | server: port: 8080 --- apiVersion: v1 kind: Secret metadata: { name: jwt-keys } stringData: public.pem: | -----BEGIN PUBLIC KEY----- ... -----END PUBLIC KEY----- |
Explicación: ConfigMap para propiedades y Secret para claves públicas.
Deployment y Service (gateway)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
apiVersion: apps/v1 kind: Deployment metadata: { name: api-gateway } spec: replicas: 2 selector: { matchLabels: { app: api-gateway } } template: metadata: { labels: { app: api-gateway } } spec: containers: - name: api-gateway image: yourrepo/api-gateway:latest ports: [{ containerPort: 8080 }] readinessProbe: { httpGet: { path: /actuator/health/readiness, port: 8080 }, initialDelaySeconds: 10 } livenessProbe: { httpGet: { path: /actuator/health/liveness, port: 8080 }, initialDelaySeconds: 30 } resources: requests: { cpu: "250m", memory: "512Mi" } limits: { cpu: "1", memory: "1Gi" } env: - name: SPRING_PROFILES_ACTIVE value: k8s volumeMounts: - name: jwt-keys mountPath: /app/jwt volumes: - name: jwt-keys secret: secretName: jwt-keys --- apiVersion: v1 kind: Service metadata: { name: api-gateway } spec: type: ClusterIP selector: { app: api-gateway } ports: - port: 80 targetPort: 8080 |
Explicación: Escala a 2 réplicas y define probes para salud.
Ingress
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: api-gateway annotations: nginx.ingress.kubernetes.io/proxy-body-size: "10m" spec: rules: - host: demo.local http: paths: - path: / pathType: Prefix backend: service: name: api-gateway port: number: 80 |
Explicación: Expone el gateway vía Ingress. Configura DNS o /etc/hosts.
Comandos:
1 2 |
kubectl apply -f k8s/ |
Checklist:
- [ ] ConfigMaps/Secrets creados
- [ ] Deployments con probes
- [ ] Ingress operativo
15) Rendimiento y buenas prácticas
- HikariCP: ajusta pool (minIdle, maxPoolSize) según carga.
- GC Java 21: G1 por defecto; ajusta MaxRAMPercentage.
- GZIP en gateway: habilita compresión.
- Cache HTTP: usa ETag y Cache-Control para GET.
- Hibernate: batchfetchsize, second-level cache si aplica.
Ejemplo Hikari:
1 2 3 4 5 6 |
spring: datasource: hikari: maximum-pool-size: 20 minimum-idle: 4 |
Explicación: Evita saturación de conexiones manteniendo latencia estable.
Checklist:
- [ ] Pool ajustado
- [ ] Compresión y cache activas
- [ ] Hibernate afinado
16) CI/CD básico (GitHub Actions)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
name: ci on: [push, pull_request] jobs: build: runs-on: ubuntu-latest services: postgres: image: postgres:16-alpine env: POSTGRES_DB: test POSTGRES_PASSWORD: test POSTGRES_USER: test ports: ["5432:5432"] steps: - uses: actions/checkout@v4 - uses: actions/setup-java@v4 with: distribution: temurin java-version: 21 - name: Build & Test run: mvn -B -DskipITs=false test package - name: Build Docker run: | docker build -t ghcr.io/you/api-gateway:$(git rev-parse --short HEAD) api-gateway - name: Login & Push run: | echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u USERNAME --password-stdin docker push ghcr.io/you/api-gateway:$(git rev-parse --short HEAD) |
Explicación: Compila, ejecuta tests (incluidos Testcontainers) y construye/push imágenes a GHCR.
Checklist:
- [ ] Pipeline compila y prueba
- [ ] Imágenes versionadas por commit
17) Probar el flujo end-to-end
Comandos cURL/HTTPie:
1 2 3 4 5 6 7 8 9 10 11 |
# Registro curl -X POST http://localhost:8080/auth/register -H 'Content-Type: application/json' \ -d '{"email":"alice@example.com","password":"Secret123"}' # Login ACCESS=$(curl -s -X POST http://localhost:8080/auth/login -H 'Content-Type: application/json' \ -d '{"email":"alice@example.com","password":"Secret123"}' | jq -r .accessToken) # Listar productos (requiere token si protegido) curl -H "Authorization: Bearer $ACCESS" http://localhost:8080/api/catalog/products |
Explicación: Flujo básico de autenticación y consumo de APIs a través del gateway.
Resolución de problemas comunes
- Puertos ocupados: cambia server.port o mapea otros puertos en docker-compose.yml.
- Variables .env faltantes: define SPRINGPROFILESACTIVE y credenciales en env o Secrets.
- Flyway falla: revisa compatibilidad de esquemas y orden de migraciones; limpia BD en dev.
- Certificados JWT: asegúrate de que gateway/servicios usan la misma clave pública que auth-service.
- Servicios no se registran en Eureka: verifica eureka.client.serviceUrl y conectividad, reloj del sistema y nombres de servicio.
- Timeouts Feign: aumenta Request.Options o agrega retries con Resilience4j.
Conclusión y llamada a la acción
Has creado un sistema de microservicios robusto con Spring Boot 3 y Spring Cloud: configuración centralizada, discovery, gateway, seguridad JWT, persistencia con PostgreSQL, documentación OpenAPI, observabilidad y despliegue en Docker y Kubernetes. Próximos pasos: añade mensajería con Kafka (notificaciones), patrones Saga/Orquestación para consistencia distribuida y tracing con Jaeger/Tempo.
¿Listo para llevarlo a producción? Crea tu repo, habilita CI/CD y despliega en tu clúster Kubernetes.
Checklist global
- [ ] Configuración centralizada con config-server y Git
- [ ] Service discovery con Eureka Service Discovery
- [ ] API Gateway con Spring Cloud Gateway (CORS, retry, gzip)
- [ ] Autenticación con JWT Spring Security (RS256, access/refresh)
- [ ] Servicios catalog y order con JPA, Flyway y PostgreSQL
- [ ] OpenFeign + Resilience4j
- [ ] Manejo de errores RFC 7807 y DTOs comunes
- [ ] OpenAPI springdoc en cada servicio
- [ ] Actuator + Prometheus + logs/tracing
- [ ] Tests con Testcontainers Spring
- [ ] Docker Spring Boot y docker-compose
- [ ] Manifiestos Kubernetes Spring Boot (Deploy/Service/Ingress)
- [ ] CI/CD con GitHub Actions