Tutorial microservicios Spring Boot: construye un sistema completo con Java, Docker, Eureka, API Gateway y JWT
Meta title: Tutorial microservicios Spring Boot con Java, Docker, Eureka, API Gateway y JWT
Meta description: Guía paso a paso para crear microservicios con Java y Spring Boot: JPA con PostgreSQL, OpenFeign, RabbitMQ, Eureka, Spring Cloud Gateway, seguridad JWT, Docker y Testcontainers.
URL slug: tutorial-microservicios-spring-boot-java-docker-eureka-api-gateway-jwt
1. Introducción: objetivos y requisitos previos
En este tutorial microservicios Spring Boot construirás, paso a paso, una arquitectura moderna de microservicios con Java. Implementaremos dos servicios (catálogo y pedidos), persistencia con Spring Data JPA y PostgreSQL, documentación OpenAPI/Swagger, comunicación sincrónica con OpenFeign (y RestTemplate), mensajería RabbitMQ (opcional), service discovery con Eureka, API Gateway con Spring Cloud Gateway, seguridad con Spring Security y JWT, contenedorización con Docker y orquestación con docker-compose. Además, veremos pruebas unitarias y de integración con JUnit 5 y Testcontainers y observabilidad básica con Spring Boot Actuator.
Objetivos:
- Crear y ejecutar microservicios con Java usando Spring Boot.
- Integrar PostgreSQL, validación, DTOs, mapeo y manejo global de errores.
- Documentar APIs con OpenAPI/Swagger.
- Comunicar servicios con OpenFeign y, opcionalmente, RabbitMQ.
- Registrar servicios con Eureka y enrutar con Spring Cloud Gateway.
- Proteger endpoints con Spring Security JWT.
- Probar con JUnit 5 y Testcontainers.
- Empaquetar y levantar todo con Docker y docker-compose.
Requisitos previos:
- Conocimientos básicos de Java, HTTP/REST y SQL.
- Sistema operativo macOS, Linux o Windows.
- Permisos para instalar software.
Checklist de esta sección:
- [ ] Comprendes el alcance del tutorial microservicios Spring Boot.
- [ ] Tienes nociones básicas de Java y Spring.
2. Instalación: JDK 17, Maven/Gradle y Docker
- Instala JDK 17 (Temurin u OpenJDK). Verifica:
1 2 3 |
a java -version # Debe mostrar 17.x |
- Instala Maven (o Gradle). Verifica:
1 2 3 4 |
mvn -v # o gradle -v |
- Instala Docker Desktop o Docker Engine + docker-compose. Verifica:
1 2 3 |
docker --version docker compose version # o docker-compose --version |
Checklist:
- [ ] JDK 17 instalado.
- [ ] Maven o Gradle funcional.
- [ ] Docker y docker-compose operativos.
3. Crear proyectos con Spring Initializr
Crearemos 5 módulos:
- eureka-server
- api-gateway
- catalog-service
- order-service
- common (opcional, para DTOs compartidos)
Usa start.spring.io (Spring Initializr) o curl.
Dependencias recomendadas por módulo:
- eureka-server: Spring Cloud Netflix Eureka Server, Actuator.
- api-gateway: Spring Cloud Gateway, Eureka Discovery Client, Actuator, Spring Security.
- catalog-service: Spring Web, Spring Data JPA, PostgreSQL Driver, Validation, OpenFeign, Actuator, Spring Security, Springdoc OpenAPI.
- order-service: Spring Web, Spring Data JPA, PostgreSQL Driver, Validation, OpenFeign, Actuator, Spring Security, Springdoc OpenAPI, Spring for RabbitMQ (opcional).
- common (opcional): Lombok, Validation.
Ejemplo con Maven (catálogo):
1 2 3 4 5 6 |
curl https://start.spring.io/starter.zip \ -d dependencies=web,data-jpa,postgresql,validation,openfeign,actuator,security \ -d javaVersion=17 -d type=maven-project -d language=java \ -d groupId=com.acme -d artifactId=catalog-service \ -o catalog-service.zip && unzip catalog-service.zip |
Checklist:
- [ ] Creaste los módulos con dependencias correctas.
- [ ] Compilan con mvn clean package.
4. Estructura de paquetes recomendada
Ejemplo para catalog-service:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
com.acme.catalog ├─ CatalogServiceApplication ├─ config/ ├─ domain/ │ ├─ model/ (entidades JPA) │ └─ repo/ (repositorios) ├─ service/ (lógica de negocio) ├─ web/ │ ├─ dto/ │ ├─ mapper/ │ ├─ controller/ │ └─ advice/ (manejo de errores) └─ client/ (Feign, RestTemplate) |
Checklist:
- [ ] Separas dominio, servicio y web.
- [ ] Preparas carpetas para DTOs, mapeo y errores.
5. Configuración base y perfiles
application.yml (catálogo) con perfiles dev y docker, Eureka, Actuator y JWT.
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 |
server: port: 0 # puerto aleatorio; el Gateway enruta vía Eureka spring: application: name: catalog-service datasource: url: jdbc:postgresql://localhost:5432/catalogdb username: catalog password: catalog jpa: hibernate: ddl-auto: update properties: hibernate.format_sql: true profiles: active: dev rabbitmq: host: localhost port: 5672 management: endpoints: web: exposure: include: health,info,metrics,env eureka: client: serviceUrl: defaultZone: http://localhost:8761/eureka/ security: jwt: secret: change-me-in-prod expiration: 3600000 --- spring: config: activate: on-profile: docker datasource: url: jdbc:postgresql://postgres:5432/catalogdb username: catalog password: catalog rabbitmq: host: rabbitmq |
Explicación: definimos datasource para PostgreSQL, exponemos Actuator, habilitamos descubrimiento en Eureka y separamos configuración para contenedores.
Checklist:
- [ ] application.yml con perfiles y Eureka.
- [ ] Actuator expuesto para observabilidad.
6. Microservicio de Catálogo: entidades, DTOs, validación y API REST
Entidad y repositorio:
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 |
// domain/model/Product.java package com.acme.catalog.domain.model; import jakarta.persistence.*; import jakarta.validation.constraints.*; import java.math.BigDecimal; @Entity public class Product { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @NotBlank private String name; @PositiveOrZero private BigDecimal price; @PositiveOrZero private Integer stock; // getters/setters } // domain/repo/ProductRepository.java package com.acme.catalog.domain.repo; import com.acme.catalog.domain.model.Product; import org.springframework.data.jpa.repository.JpaRepository; public interface ProductRepository extends JpaRepository<Product, Long> {} |
DTOs y mapeo manual:
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 |
// web/dto/ProductDto.java package com.acme.catalog.web.dto; import jakarta.validation.constraints.*; import java.math.BigDecimal; public record ProductDto( Long id, @NotBlank String name, @PositiveOrZero BigDecimal price, @PositiveOrZero Integer stock ) {} // web/mapper/ProductMapper.java package com.acme.catalog.web.mapper; import com.acme.catalog.domain.model.Product; import com.acme.catalog.web.dto.ProductDto; public class ProductMapper { public static ProductDto toDto(Product p) { return new ProductDto(p.getId(), p.getName(), p.getPrice(), p.getStock()); } public static Product toEntity(ProductDto d) { Product p = new Product(); p.setId(d.id()); p.setName(d.name()); p.setPrice(d.price()); p.setStock(d.stock()); return p; } } |
Servicio y controlador con validación Bean Validation:
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 |
// service/ProductService.java package com.acme.catalog.service; import com.acme.catalog.domain.model.Product; import com.acme.catalog.domain.repo.ProductRepository; import org.springframework.stereotype.Service; import java.util.List; @Service public class ProductService { private final ProductRepository repo; public ProductService(ProductRepository repo) { this.repo = repo; } public List<Product> findAll() { return repo.findAll(); } public Product findById(Long id) { return repo.findById(id).orElseThrow(() -> new NotFoundException("Product not found")); } public Product create(Product p) { return repo.save(p); } public Product update(Long id, Product p) { var existing = findById(id); p.setId(existing.getId()); return repo.save(p); } public void delete(Long id) { repo.deleteById(id); } } // web/advice/NotFoundException.java package com.acme.catalog.service; public class NotFoundException extends RuntimeException { public NotFoundException(String m){super(m);} } // web/advice/GlobalExceptionHandler.java package com.acme.catalog.web.advice; import com.acme.catalog.service.NotFoundException; import org.springframework.http.*; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.*; import java.util.*; @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(NotFoundException.class) public ResponseEntity<Map<String,Object>> handleNotFound(NotFoundException ex){ return ResponseEntity.status(HttpStatus.NOT_FOUND).body(Map.of("error", ex.getMessage())); } @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<Map<String,Object>> handleValidation(MethodArgumentNotValidException ex){ Map<String,String> errors = new HashMap<>(); ex.getBindingResult().getFieldErrors().forEach(e -> errors.put(e.getField(), e.getDefaultMessage())); return ResponseEntity.badRequest().body(Map.of("validationErrors", errors)); } } // web/controller/ProductController.java package com.acme.catalog.web.controller; import com.acme.catalog.service.ProductService; import com.acme.catalog.web.dto.ProductDto; import com.acme.catalog.web.mapper.ProductMapper; import jakarta.validation.Valid; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; import java.util.List; @RestController @RequestMapping("/api/v1/products") public class ProductController { private final ProductService service; public ProductController(ProductService s){this.service=s;} @GetMapping public List<ProductDto> all(){ return service.findAll().stream().map(ProductMapper::toDto).toList(); } @GetMapping("/{id}") public ProductDto byId(@PathVariable Long id){ return ProductMapper.toDto(service.findById(id)); } @PostMapping @ResponseStatus(HttpStatus.CREATED) public ProductDto create(@RequestBody @Valid ProductDto dto){ return ProductMapper.toDto(service.create(ProductMapper.toEntity(dto))); } @PutMapping("/{id}") public ProductDto update(@PathVariable Long id, @RequestBody @Valid ProductDto dto){ return ProductMapper.toDto(service.update(id, ProductMapper.toEntity(dto))); } @DeleteMapping("/{id}") @ResponseStatus(HttpStatus.NO_CONTENT) public void delete(@PathVariable Long id){ service.delete(id); } } |
Checklist:
- [ ] Entidad, repositorio y servicio creados.
- [ ] DTOs, validación y mapeo funcionando.
- [ ] Manejo global de errores con @ControllerAdvice.
7. Microservicio de Pedidos: dominio, comunicación con catálogo y API REST
Entidad, ítems y repositorios:
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 |
// domain/model/Order.java package com.acme.order.domain.model; import jakarta.persistence.*; import java.math.BigDecimal; import java.util.*; @Entity @Table(name="orders") public class Order { @Id @GeneratedValue(strategy=GenerationType.IDENTITY) private Long id; private BigDecimal total; @OneToMany(cascade=CascadeType.ALL, orphanRemoval=true) @JoinColumn(name="order_id") private List<OrderItem> items = new ArrayList<>(); // getters/setters } // domain/model/OrderItem.java @Entity public class OrderItem { @Id @GeneratedValue(strategy=GenerationType.IDENTITY) private Long id; private Long productId; private Integer quantity; private BigDecimal unitPrice; // getters/setters } // domain/repo/OrderRepository.java package com.acme.order.domain.repo; import com.acme.order.domain.model.Order; import org.springframework.data.jpa.repository.JpaRepository; public interface OrderRepository extends JpaRepository<Order, Long> {} |
Feign Client para consultar catálogo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// client/ProductClient.java package com.acme.order.client; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.*; import java.math.BigDecimal; record ProductResponse(Long id, String name, BigDecimal price, Integer stock) {} @FeignClient(name = "catalog-service") public interface ProductClient { @GetMapping("/api/v1/products/{id}") ProductResponse getById(@PathVariable Long id); } |
Servicio de pedidos:
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 |
// service/OrderService.java package com.acme.order.service; import com.acme.order.client.ProductClient; import com.acme.order.domain.model.*; import com.acme.order.domain.repo.OrderRepository; import org.springframework.stereotype.Service; import java.math.BigDecimal; @Service public class OrderService { private final OrderRepository repo; private final ProductClient products; public OrderService(OrderRepository r, ProductClient p){ this.repo=r; this.products=p; } public Order create(Order o){ BigDecimal total = BigDecimal.ZERO; for (OrderItem item : o.getItems()) { var p = products.getById(item.getProductId()); item.setUnitPrice(p.price()); total = total.add(p.price().multiply(BigDecimal.valueOf(item.getQuantity()))); } o.setTotal(total); return repo.save(o); } } |
Controlador y DTO simple:
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 |
// web/dto/CreateOrderDto.java package com.acme.order.web.dto; import jakarta.validation.constraints.*; import java.util.List; public record CreateOrderDto(@NotEmpty List<Item> items) { public static record Item(@NotNull Long productId, @Positive Integer quantity){} } // web/controller/OrderController.java package com.acme.order.web.controller; import com.acme.order.domain.model.*; import com.acme.order.service.OrderService; import com.acme.order.web.dto.CreateOrderDto; import jakarta.validation.Valid; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/v1/orders") public class OrderController { private final OrderService service; public OrderController(OrderService s){this.service=s;} @PostMapping @ResponseStatus(HttpStatus.CREATED) public Order create(@RequestBody @Valid CreateOrderDto dto){ Order o = new Order(); dto.items().forEach(i -> { OrderItem it = new OrderItem(); it.setProductId(i.productId()); it.setQuantity(i.quantity()); o.getItems().add(it); }); return service.create(o); } } |
Opcional: RestTemplate como fallback para Feign en casos simples.
Checklist:
- [ ] Dominio de pedidos creado.
- [ ] Comunicación sincrónica con OpenFeign funcionando.
8. OpenAPI/Swagger con springdoc-openapi
Agrega dependencia en los servicios:
1 2 3 4 5 6 7 |
<!-- pom.xml --> <dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> <version>2.5.0</version> </dependency> |
Accede a /swagger-ui.html para ver la documentación generada automáticamente.
Checklist:
- [ ] Swagger UI accesible.
9. Mensajería asíncrona (opcional) con RabbitMQ
Publica un evento al crear pedido:
1 2 3 4 5 6 7 8 9 10 11 12 |
// service/OrderEventPublisher.java package com.acme.order.service; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.stereotype.Component; @Component public class OrderEventPublisher { private final RabbitTemplate rabbit; public OrderEventPublisher(RabbitTemplate r){this.rabbit=r;} public void orderCreated(Long orderId){ rabbit.convertAndSend("orders.exchange", "order.created", orderId); } } |
Configura exchange/queue/binding (simplificado) y llama publisher.orderCreated(o.getId()) tras guardar el pedido.
Checklist:
- [ ] RabbitMQ instalado y variables configuradas.
- [ ] Evento emitido al crear pedido.
10. Service Discovery con Eureka y API Gateway con Spring Cloud Gateway
Eureka Server aplicación y configuración:
1 2 3 4 5 |
// EurekaServerApplication.java @SpringBootApplication @EnableEurekaServer public class EurekaServerApplication { public static void main(String[] args){ SpringApplication.run(EurekaServerApplication.class,args);} } |
1 2 3 4 5 6 7 8 9 10 11 |
# eureka-server/src/main/resources/application.yml server: { port: 8761 } spring: application: name: eureka-server eureka: client: register-with-eureka: false fetch-registry: false |
Gateway con rutas basadas en Eureka:
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 |
# api-gateway/src/main/resources/application.yml server: { port: 8080 } spring: application: name: api-gateway cloud: gateway: discovery: locator: enabled: true lowerCaseServiceId: true routes: - id: catalog uri: lb://catalog-service predicates: - Path=/api/v1/products/** - id: orders uri: lb://order-service predicates: - Path=/api/v1/orders/** eureka: client: serviceUrl: defaultZone: http://eureka-server:8761/eureka/ |
Checklist:
- [ ] Eureka server corriendo en 8761.
- [ ] Gateway enruta a servicios vía lb://.
11. Seguridad con Spring Security y JWT
Dependencias ya incluidas. Implementamos un filtro JWT simple y un endpoint de login.
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 |
// security/JwtUtil.java package com.acme.shared.security; import io.jsonwebtoken.*; import io.jsonwebtoken.security.Keys; import java.security.Key; import java.util.Date; public class JwtUtil { private final Key key; private final long exp; public JwtUtil(String secret, long exp){ this.key = Keys.hmacShaKeyFor(secret.getBytes()); this.exp = exp; } public String generate(String username){ var now = new Date(); return Jwts.builder().setSubject(username).setIssuedAt(now).setExpiration(new Date(now.getTime()+exp)).signWith(key, SignatureAlgorithm.HS256).compact(); } public Jws<Claims> parse(String token){ return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token); } } // security/JwtAuthFilter.java package com.acme.shared.security; import jakarta.servlet.*; import jakarta.servlet.http.*; import org.springframework.security.authentication.*; import org.springframework.security.core.*; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.filter.OncePerRequestFilter; public class JwtAuthFilter extends OncePerRequestFilter { private final JwtUtil jwt; public JwtAuthFilter(JwtUtil j){this.jwt=j;} @Override protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws java.io.IOException, ServletException { String header = req.getHeader("Authorization"); if (header!=null && header.startsWith("Bearer ")) { String token = header.substring(7); try { var claims = jwt.parse(token).getBody(); Authentication auth = new UsernamePasswordAuthenticationToken(claims.getSubject(), null, java.util.List.of()); SecurityContextHolder.getContext().setAuthentication(auth); } catch (Exception ignored) {} } chain.doFilter(req,res); } } // security/WebSecurityConfig.java (en cada servicio) @EnableWebSecurity public class WebSecurityConfig { @Bean JwtUtil jwtUtil(@Value("${security.jwt.secret}") String secret, @Value("${security.jwt.expiration}") long exp){ return new JwtUtil(secret, exp); } @Bean SecurityFilterChain filter(HttpSecurity http, JwtUtil jwt) throws Exception { http.csrf(csrf->csrf.disable()) .authorizeHttpRequests(auth->auth .requestMatchers("/actuator/**","/swagger-ui/**","/v3/api-docs/**","/auth/login").permitAll() .anyRequest().authenticated()) .addFilterBefore(new JwtAuthFilter(jwt), UsernamePasswordAuthenticationFilter.class); return http.build(); } } // web/controller/AuthController.java @RestController @RequestMapping("/auth") public class AuthController { private final JwtUtil jwt; public AuthController(JwtUtil j){this.jwt=j;} record LoginRequest(String username, String password){} record TokenResponse(String token){} @PostMapping("/login") public TokenResponse login(@RequestBody LoginRequest req){ // Demo: valida usuario fijo. En producción, integra UserDetailsService. if ("admin".equals(req.username()) && "admin".equals(req.password())) { return new TokenResponse(jwt.generate(req.username())); } throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid credentials"); } } |
Prueba: obtiene token via POST /auth/login y úsalo en Authorization: Bearer
Checklist:
- [ ] Filtro JWT activo y /auth/login operativo.
- [ ] Endpoints protegidos.
12. Pruebas unitarias e integración (JUnit 5 y Testcontainers)
Prueba unitaria de ProductService con Mockito:
1 2 3 4 5 6 7 |
// ProductServiceTest.java @ExtendWith(MockitoExtension.class) class ProductServiceTest { @Mock ProductRepository repo; @InjectMocks ProductService service; @Test void findById_found(){ var p = new Product(); p.setId(1L); when(repo.findById(1L)).thenReturn(Optional.of(p)); assertEquals(1L, service.findById(1L).getId()); } } |
Integración con Testcontainers para PostgreSQL:
1 2 3 4 5 6 7 8 9 10 11 |
// ProductRepositoryIT.java @Testcontainers @DataJpaTest @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) class ProductRepositoryIT { @Container static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15").withDatabaseName("catalogdb").withUsername("catalog").withPassword("catalog"); @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 save_and_find(){ Product p = new Product(); p.setName("Pen"); p.setPrice(new BigDecimal("1.99")); p.setStock(10); repo.save(p); assertEquals(1, repo.findAll().size()); } } |
Checklist:
- [ ] Pruebas unitarias ejecutan.
- [ ] Testcontainers levanta PostgreSQL efímero.
13. Contenerización: Dockerfiles y docker-compose
Dockerfile genérico para servicios Spring Boot (Maven):
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# Dockerfile FROM maven:3.9.6-eclipse-temurin-17 AS build WORKDIR /app COPY pom.xml . COPY src ./src RUN mvn -q -DskipTests package FROM eclipse-temurin:17-jre WORKDIR /app COPY --from=build /app/target/*.jar app.jar ENV JAVA_OPTS="" ENTRYPOINT ["sh","-c","java $JAVA_OPTS -jar app.jar"] |
docker-compose.yml con PostgreSQL, RabbitMQ, Eureka, Gateway y servicios:
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 |
services: postgres: image: postgres:15 environment: POSTGRES_DB: catalogdb POSTGRES_USER: catalog POSTGRES_PASSWORD: catalog ports: ["5432:5432"] healthcheck: { test: ["CMD-SHELL","pg_isready -U catalog"], interval: 5s, timeout: 5s, retries: 10 } rabbitmq: image: rabbitmq:3-management ports: ["5672:5672","15672:15672"] eureka-server: build: ./eureka-server ports: ["8761:8761"] api-gateway: build: ./api-gateway environment: - EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://eureka-server:8761/eureka/ ports: ["8080:8080"] depends_on: [eureka-server] catalog-service: build: ./catalog-service environment: - SPRING_PROFILES_ACTIVE=docker - EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://eureka-server:8761/eureka/ - SECURITY_JWT_SECRET=change-me-in-prod depends_on: [postgres, eureka-server] order-service: build: ./order-service environment: - SPRING_PROFILES_ACTIVE=docker - EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://eureka-server:8761/eureka/ - SECURITY_JWT_SECRET=change-me-in-prod depends_on: [postgres, eureka-server] |
Explicación: usamos perfiles docker, services conectados por red default de compose y Eureka para descubrimiento. Gateway expone 8080.
Comandos útiles:
1 2 3 4 |
docker compose build docker compose up -d docker compose logs -f api-gateway |
Checklist:
- [ ] Dockerfiles creados para cada servicio.
- [ ] docker-compose levanta toda la plataforma.
14. Observabilidad básica con Spring Boot Actuator
- Endpoints: /actuator/health, /actuator/info, /actuator/metrics.
- Agrega info adicional en application.yml si lo deseas:
1 2 3 4 5 |
info: app: name: catalog-service version: 1.0.0 |
Checklist:
- [ ] Health y métricas expuestas y protegidas según necesidad.
15. Resolución de problemas comunes (Troubleshooting)
- Error de conexión a PostgreSQL: verifica URL, usuario y password; en docker-compose usa el host postgres y perfil docker.
- No se registran servicios en Eureka: confirma EUREKACLIENTSERVICEURL_DEFAULTZONE y clock skew del sistema.
- 401 Unauthorized al consumir API: agrega Authorization: Bearer
y verifica secret/exp. - Swagger no carga: asegura springdoc dependency y rutas públicas en Security.
- Fallan tests con Testcontainers en Windows: habilita WSL2 o Docker Desktop y aumenta memoria si es necesario.
- Feign 404: revisa path exacto del endpoint en catalog-service y el context-path.
- Gateway 503: espera a que Eureka registre instancias (unos segundos) y usa retries si aplica.
16. Buenas prácticas y próximos pasos
Buenas prácticas:
- Mantén contratos claros con DTOs y versiona tus APIs (v1, v2…).
- Centraliza errores con @ControllerAdvice y mensajes consistentes.
- Evita lógica en controladores; usa servicios y repositorios bien definidos.
- En producción, externaliza configuración (Spring Cloud Config, Vault).
- Observabilidad: añade tracing (Sleuth/Micrometer/OTel), logs estructurados y dashboards.
- Seguridad: almacena secretos en variables seguras, rota llaves JWT y usa HTTPS.
- Resiliencia: timeouts, retries y circuit breakers (Resilience4j) para OpenFeign.
- Base de datos: migraciones con Flyway o Liquibase.
Próximos pasos:
- Añade un microservicio de usuarios y roles con almacenamiento real.
- Implementa idempotencia para pedidos y compensaciones con mensajería.
- Despliegue en Kubernetes con Helm/ArgoCD y observabilidad con Prometheus/Grafana.
17. FAQ (orientado a featured snippets)
- ¿Qué es un microservicio en Spring Boot? Es una aplicación pequeña y autónoma que expone APIs y se despliega de forma independiente, comúnmente comunicándose via HTTP o mensajería.
- ¿Cómo documentar APIs en Spring Boot? Con springdoc-openapi: agrega la dependencia y accede a /swagger-ui.html.
- ¿Cómo asegurar APIs con JWT en Spring Security? Implementa un filtro que valide el token en cada request y protege rutas por roles.
- ¿Qué es Spring Cloud Gateway? Un API Gateway reactivo que enruta peticiones a microservicios usando rutas y filtros, integrado con Eureka.
- ¿Cómo usar OpenFeign? Declara interfaces con @FeignClient y anota métodos con rutas HTTP; Spring las implementa y gestiona balanceo con Eureka.
- ¿Para qué sirve Testcontainers? Para ejecutar dependencias reales (como PostgreSQL) en contenedores durante tests de integración.
- ¿Cómo desplegar con Docker? Crea Dockerfiles por servicio y orquesta con docker-compose; configura perfiles docker en application.yml.
Comandos de verificación rápidos:
- Generar token: curl -X POST http://localhost:8080/auth/login -H ‘Content-Type: application/json’ -d ‘{“username”:”admin”,”password”:”admin”}’
- Listar productos (con token): curl http://localhost:8080/api/v1/products -H “Authorization: Bearer
“ - Crear pedido: curl -X POST http://localhost:8080/api/v1/orders -H “Authorization: Bearer
” -H ‘Content-Type: application/json’ -d ‘{“items”:[{“productId”:1,”quantity”:2}]}’