Objetivo del capítulo
En este capítulo crearás un microservicio Spring Boot desde cero con una estructura de proyecto mantenible. Verás dependencias esenciales, configuración base, organización de paquetes (por capas o por dominio), convenciones de nombres, perfiles (dev/test/prod), propiedades externas y un ejemplo funcional con endpoint de salud, puerto configurado y una primera entidad/DTO. También dejarás el servicio listo para crecer: pruebas, configuración y límites claros.
Creación del proyecto y dependencias esenciales
1) Generar el proyecto
Puedes crear el proyecto con Spring Initializr (web) o desde tu IDE. Define:
- Group:
com.acme - Artifact:
catalog-service - Name:
catalog-service - Packaging:
jar - Java: 17+ (recomendado)
2) Dependencias recomendadas (mínimas pero escalables)
Para un microservicio típico, estas dependencias te dan un arranque sólido:
- Spring Web: controladores REST y servidor embebido.
- Spring Boot Actuator: endpoints operativos (salud, métricas, info).
- Validation: validación de DTOs con anotaciones.
- Spring Data JPA (opcional si usarás BD relacional) y un driver (H2 para dev/test, PostgreSQL/MySQL para prod).
- Lombok (opcional): reduce boilerplate (si tu equipo lo acepta).
- Spring Boot Test: ya viene, para pruebas unitarias e integración.
Ejemplo de pom.xml (Maven) con un set común:
<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-validation</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency></dependencies>Estructura de proyecto: por capas vs por dominio
La estructura debe ayudarte a evolucionar el servicio sin que se convierta en un “paquete gigante” difícil de navegar. Hay dos enfoques comunes; elige uno y sé consistente.
- Escuche el audio con la pantalla apagada.
- Obtenga un certificado al finalizar.
- ¡Más de 5000 cursos para que explores!
Descargar la aplicación
Opción A: organización por capas (layered)
Útil cuando el servicio es pequeño o el equipo está muy acostumbrado a capas clásicas.
com.acme.catalogservice ├─ CatalogServiceApplication ├─ config ├─ controller ├─ service ├─ repository ├─ domain │ ├─ model │ └─ dto └─ exceptionVentaja: simple y familiar. Riesgo: con el tiempo, cada capa crece y se vuelve difícil ubicar “todo lo de un caso de uso”.
Opción B: organización por dominio/feature (recomendada para microservicios)
Agrupa por “capacidad” o “subdominio” dentro del microservicio, manteniendo juntas las piezas relacionadas.
com.acme.catalogservice ├─ CatalogServiceApplication ├─ shared │ ├─ config │ ├─ error │ └─ web └─ product ├─ api │ ├─ ProductController │ └─ dto ├─ application │ └─ ProductService ├─ domain │ └─ Product └─ infrastructure └─ ProductRepositoryVentaja: escalabilidad organizativa; es más fácil mantener límites y ubicar cambios. Recomendación: usa este enfoque si esperas que el servicio crezca o tenga varios módulos funcionales.
Convenciones de nombres y límites del microservicio
Convenciones prácticas
- Nombre del servicio:
catalog-service(kebab-case) para despliegue/infra; paquete Java en lower-case sin guiones:com.acme.catalogservice. - Controladores: sufijo
Controller(p. ej.,ProductController). - Servicios de aplicación: sufijo
Service(p. ej.,ProductService). - DTOs: sufijos
Request/Response(p. ej.,CreateProductRequest,ProductResponse). - Entidades: nombres de dominio (p. ej.,
Product), evita sufijos comoEntitysalvo colisiones. - Rutas REST: recursos en plural:
/api/products.
Cómo mantener el servicio autocontenido
- Evita dependencias cruzadas con otros microservicios a nivel de código (no compartas módulos de dominio entre servicios; comparte contratos vía OpenAPI/AsyncAPI o librerías muy pequeñas y estables si es imprescindible).
- Configura Actuator y health checks desde el inicio.
- Incluye pruebas unitarias y al menos una de integración (contexto + endpoint).
- Propiedades externas y perfiles listos para entornos.
Configuración base: puerto, Actuator y perfiles
1) Configuración común (application.yml)
Define lo mínimo común a todos los entornos. Ejemplo:
server: port: 8081spring: application: name: catalog-servicemanagement: endpoints: web: exposure: include: health,info endpoint: health: probes: enabled: trueNotas:
server.portfija el puerto del servicio (útil cuando ejecutas varios localmente).spring.application.nameayuda en logs, trazas y configuración externa.management.endpoints.web.exposure.includeexpone endpoints de Actuator seleccionados (no expongas todo por defecto).
2) Perfiles dev/test/prod
Usa archivos por perfil para separar configuración por entorno:
application-dev.ymlapplication-test.ymlapplication-prod.yml
Ejemplo application-dev.yml con H2 en memoria:
spring: datasource: url: jdbc:h2:mem:catalog;MODE=PostgreSQL;DB_CLOSE_DELAY=-1 driverClassName: org.h2.Driver username: sa password: jpa: hibernate: ddl-auto: update properties: hibernate: format_sql: truelogging: level: org.hibernate.SQL: DEBUGEjemplo application-test.yml (más estricto y reproducible):
spring: jpa: hibernate: ddl-auto: create-drop datasource: url: jdbc:h2:mem:catalog_test;MODE=PostgreSQL;DB_CLOSE_DELAY=-1Ejemplo application-prod.yml (sin credenciales hardcodeadas):
spring: datasource: url: ${DB_URL} username: ${DB_USERNAME} password: ${DB_PASSWORD} jpa: hibernate: ddl-auto: validate3) Activar un perfil
Opciones típicas:
- Variable de entorno:
SPRING_PROFILES_ACTIVE=dev - Argumento al arrancar:
--spring.profiles.active=dev - En tests: anotación
@ActiveProfiles("test")
Gestión de propiedades externas (12-factor friendly)
Para que el servicio sea portable entre entornos, evita valores sensibles en el repositorio. Usa placeholders y variables de entorno.
Ejemplo de propiedades externas
En application-prod.yml ya viste ${DB_URL}, ${DB_USERNAME}, ${DB_PASSWORD}. Puedes aplicar lo mismo a:
- URLs de servicios externos
- API keys
- timeouts
- feature flags
Recomendación: define defaults seguros solo para dev, y exige variables en prod. Si necesitas validar que existan, puedes usar ${VAR:?error} en algunos shells o validar al inicio con @ConfigurationProperties + @Validated.
Ejemplo completo de arranque: salud, puerto y primera entidad/DTO
1) Clase principal
package com.acme.catalogservice;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplicationpublic class CatalogServiceApplication { public static void main(String[] args) { SpringApplication.run(CatalogServiceApplication.class, args); }}2) Endpoint de salud
Con Actuator, el endpoint de salud ya existe en /actuator/health. Si quieres un endpoint “simple” adicional (útil para balanceadores antiguos o checks muy básicos), puedes crear uno:
package com.acme.catalogservice.shared.web;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RestController;import java.util.Map;@RestControllerpublic class HealthController { @GetMapping("/health") public Map<String, String> health() { return Map.of("status", "UP"); }}Recomendación: en entornos modernos, prioriza /actuator/health y configura su exposición adecuadamente.
3) Primera entidad de dominio y DTOs
Ejemplo de dominio “Product” con una API mínima de creación y consulta. Estructura sugerida por dominio:
product/domain/Productproduct/api/dto/CreateProductRequestproduct/api/dto/ProductResponse
Entidad JPA:
package com.acme.catalogservice.product.domain;import jakarta.persistence.*;import java.math.BigDecimal;import java.util.UUID;@Entity@Table(name = "products")public class Product { @Id @GeneratedValue private UUID id; @Column(nullable = false, length = 120) private String name; @Column(nullable = false, precision = 12, scale = 2) private BigDecimal price; protected Product() {} public Product(String name, BigDecimal price) { this.name = name; this.price = price; } public UUID getId() { return id; } public String getName() { return name; } public BigDecimal getPrice() { return price; }}DTO de entrada con validación:
package com.acme.catalogservice.product.api.dto;import jakarta.validation.constraints.*;import java.math.BigDecimal;public class CreateProductRequest { @NotBlank @Size(max = 120) private String name; @NotNull @DecimalMin(value = "0.0", inclusive = false) private BigDecimal price; public String getName() { return name; } public void setName(String name) { this.name = name; } public BigDecimal getPrice() { return price; } public void setPrice(BigDecimal price) { this.price = price; }}DTO de salida:
package com.acme.catalogservice.product.api.dto;import java.math.BigDecimal;import java.util.UUID;public class ProductResponse { private UUID id; private String name; private BigDecimal price; public ProductResponse(UUID id, String name, BigDecimal price) { this.id = id; this.name = name; this.price = price; } public UUID getId() { return id; } public String getName() { return name; } public BigDecimal getPrice() { return price; }}4) Repositorio (infraestructura)
package com.acme.catalogservice.product.infrastructure;import com.acme.catalogservice.product.domain.Product;import org.springframework.data.jpa.repository.JpaRepository;import java.util.UUID;public interface ProductRepository extends JpaRepository<Product, UUID> {}5) Servicio de aplicación
Centraliza la lógica del caso de uso (crear producto). Mantén el controlador delgado.
package com.acme.catalogservice.product.application;import com.acme.catalogservice.product.api.dto.CreateProductRequest;import com.acme.catalogservice.product.api.dto.ProductResponse;import com.acme.catalogservice.product.domain.Product;import com.acme.catalogservice.product.infrastructure.ProductRepository;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;@Servicepublic class ProductService { private final ProductRepository repository; public ProductService(ProductRepository repository) { this.repository = repository; } @Transactional public ProductResponse create(CreateProductRequest request) { Product saved = repository.save(new Product(request.getName(), request.getPrice())); return new ProductResponse(saved.getId(), saved.getName(), saved.getPrice()); }}6) Controlador REST
package com.acme.catalogservice.product.api;import com.acme.catalogservice.product.api.dto.CreateProductRequest;import com.acme.catalogservice.product.api.dto.ProductResponse;import com.acme.catalogservice.product.application.ProductService;import jakarta.validation.Valid;import org.springframework.http.HttpStatus;import org.springframework.web.bind.annotation.*;@RestController@RequestMapping("/api/products")public class ProductController { private final ProductService service; public ProductController(ProductService service) { this.service = service; } @PostMapping @ResponseStatus(HttpStatus.CREATED) public ProductResponse create(@Valid @RequestBody CreateProductRequest request) { return service.create(request); }}7) Probar el arranque local
Arranca con perfil dev:
./mvnw spring-boot:run -Dspring-boot.run.profiles=devVerifica:
GET http://localhost:8081/actuator/healthGET http://localhost:8081/health(si lo agregaste)POST http://localhost:8081/api/productscon body JSON:
{ "name": "Keyboard", "price": 49.99}Pruebas listas para escalar: unitarias e integración
1) Prueba de controlador (slice test con MockMvc)
Este test valida la capa web sin levantar toda la infraestructura (rápido y enfocado). Aquí simulamos el servicio:
package com.acme.catalogservice.product.api;import com.acme.catalogservice.product.api.dto.ProductResponse;import com.acme.catalogservice.product.application.ProductService;import org.junit.jupiter.api.Test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;import org.springframework.boot.test.mock.mockito.MockBean;import org.springframework.http.MediaType;import org.springframework.test.web.servlet.MockMvc;import java.math.BigDecimal;import java.util.UUID;import static org.mockito.ArgumentMatchers.any;import static org.mockito.Mockito.when;import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;@WebMvcTest(ProductController.class)class ProductControllerTest { @Autowired MockMvc mvc; @MockBean ProductService service; @Test void createsProduct() throws Exception { UUID id = UUID.randomUUID(); when(service.create(any())).thenReturn(new ProductResponse(id, "Keyboard", new BigDecimal("49.99"))); mvc.perform(post("/api/products") .contentType(MediaType.APPLICATION_JSON) .content("{\"name\":\"Keyboard\",\"price\":49.99}")) .andExpect(status().isCreated()) .andExpect(jsonPath("$.id").exists()) .andExpect(jsonPath("$.name").value("Keyboard")); }}2) Prueba de integración mínima (contexto + endpoint de salud)
Este test levanta el contexto completo con perfil test y valida que el servicio arranca correctamente.
package com.acme.catalogservice;import org.junit.jupiter.api.Test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.boot.test.web.client.TestRestTemplate;import org.springframework.test.context.ActiveProfiles;import static org.assertj.core.api.Assertions.assertThat;@ActiveProfiles("test")@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)class HealthIntegrationTest { @Autowired TestRestTemplate rest; @Test void actuatorHealthIsUp() { var response = rest.getForEntity("/actuator/health", String.class); assertThat(response.getStatusCode().is2xxSuccessful()).isTrue(); assertThat(response.getBody()).contains("UP"); }}Checklist de autocontención y escalabilidad inmediata
| Área | Qué dejar listo | Ejemplo/nota |
|---|---|---|
| Configuración | Perfiles separados y propiedades externas | application-dev.yml, application-prod.yml con ${ENV_VAR} |
| Operación | Actuator con exposición mínima | health,info y probes habilitadas |
| Estructura | Paquetes por dominio o capas, consistente | Preferible por dominio para crecer |
| API | DTOs con validación y controladores delgados | @Valid en requests |
| Persistencia | Repositorio aislado en infraestructura | JPA repository en paquete infrastructure |
| Pruebas | Al menos un slice test y un integration test | @WebMvcTest y @SpringBootTest |