Creación de microservicios con Spring Boot y estructura de proyecto

Capítulo 2

Tiempo estimado de lectura: 10 minutos

+ Ejercicio

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.

Continúa en nuestra aplicación.
  • Escuche el audio con la pantalla apagada.
  • Obtenga un certificado al finalizar.
  • ¡Más de 5000 cursos para que explores!
O continúa leyendo más abajo...
Download App

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  └─ exception

Ventaja: 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          └─ ProductRepository

Ventaja: 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 como Entity salvo 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: true

Notas:

  • server.port fija el puerto del servicio (útil cuando ejecutas varios localmente).
  • spring.application.name ayuda en logs, trazas y configuración externa.
  • management.endpoints.web.exposure.include expone 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.yml
  • application-test.yml
  • application-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: DEBUG

Ejemplo 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=-1

Ejemplo application-prod.yml (sin credenciales hardcodeadas):

spring:  datasource:    url: ${DB_URL}    username: ${DB_USERNAME}    password: ${DB_PASSWORD}  jpa:    hibernate:      ddl-auto: validate

3) 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/Product
  • product/api/dto/CreateProductRequest
  • product/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=dev

Verifica:

  • GET http://localhost:8081/actuator/health
  • GET http://localhost:8081/health (si lo agregaste)
  • POST http://localhost:8081/api/products con 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

ÁreaQué dejar listoEjemplo/nota
ConfiguraciónPerfiles separados y propiedades externasapplication-dev.yml, application-prod.yml con ${ENV_VAR}
OperaciónActuator con exposición mínimahealth,info y probes habilitadas
EstructuraPaquetes por dominio o capas, consistentePreferible por dominio para crecer
APIDTOs con validación y controladores delgados@Valid en requests
PersistenciaRepositorio aislado en infraestructuraJPA repository en paquete infrastructure
PruebasAl menos un slice test y un integration test@WebMvcTest y @SpringBootTest

Ahora responde el ejercicio sobre el contenido:

Al estructurar un microservicio Spring Boot para que sea mantenible y fácil de escalar, ¿qué enfoque facilita más mantener juntas las piezas relacionadas y ubicar cambios por capacidad funcional?

¡Tienes razón! Felicitaciones, ahora pasa a la página siguiente.

¡Tú error! Inténtalo de nuevo.

La organización por dominio/feature mantiene juntas las clases relacionadas (API, aplicación, dominio e infraestructura) de una misma capacidad, lo que ayuda a escalar el servicio y a encontrar cambios más rápido.

Siguiente capítulo

APIs REST escalables en Spring Boot

Arrow Right Icon
Portada de libro electrónico gratuitaMicroservicios con Spring Boot desde Cero
17%

Microservicios con Spring Boot desde Cero

Nuevo curso

12 páginas

Descarga la aplicación para obtener una certificación gratuita y escuchar cursos en segundo plano, incluso con la pantalla apagada.