Profiles en Spring Boot: configuración dev, test, prod

1. Descripción general

Una aplicación típicamente se ejecuta en múltiples entornos. Tu máquina usa base de datos H2. El servidor de test usa PostgreSQL separado. Producción usa un cluster real con SMTP real. Si modificas manualmente el archivo de configuración cada vez que despliegas, tarde o temprano terminarás subiendo la configuración de dev a producción.

Spring Boot resuelve esto usando profiles. Después de leer este artículo, sabrás cómo separar archivos de configuración por profile, habilitar/deshabilitar beans con @Profile, activar profiles de múltiples formas, escribir tests con @ActiveProfiles y evitar errores comunes.

2. ¿Qué es un Profile?

Un profile es una etiqueta con nombre, por ejemplo dev, test, prod. Spring usa esta etiqueta para decidir dos cosas:

  • Qué configuración se carga: application-dev.yml solo se lee cuando el profile dev está activo.
  • Qué beans se crean: un bean anotado con @Profile("prod") solo se registra cuando el profile prod está activo.

Si ningún profile está activado, Spring usa el profile por defecto llamado default. Un bean sin anotación @Profile siempre se crea, independientemente de qué profile esté activo.

Solo necesitas las dependencias web básicas para los ejemplos en este artículo:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

3. Archivos de configuración por profile

3.1. Separar en múltiples archivos

La convención es nombrar archivos como application-{profile}.yml (o .properties). El archivo application.yml contiene la configuración común que siempre se carga:

# application.yml
spring:
  application:
    name: shop-service
  profiles:
    default: dev

app:
  mail:
    from: no-reply@shop.vn
    host: localhost

Los archivos según el profile solo incluyen las diferencias:

# application-dev.yml
server:
  port: 8080
logging:
  level:
    root: DEBUG
# application-test.yml
server:
  port: 0
app:
  mail:
    host: fake-smtp
# application-prod.yml
server:
  port: 80
logging:
  level:
    root: WARN
app:
  mail:
    host: ${MAIL_HOST}

Explicación:

  • spring.profiles.default: dev significa que cuando no activas ningún profile, Spring usa dev en lugar de default. Tu máquina de desarrollo funciona de inmediato sin parámetros adicionales.
  • Cuando el profile prod está activo, Spring carga primero application.yml y luego application-prod.yml. Los valores en el archivo del profile sobrescriben los valores comunes. Por lo tanto, app.mail.host se convierte en el valor de la variable de entorno MAIL_HOST, mientras que app.mail.from conserva no-reply@shop.vn.
  • server.port: 0 en test permite que Spring elija un puerto aleatorio, evitando conflictos al ejecutar tests en paralelo.

3.2. Combinar en un único archivo YAML

Si quieres menos archivos, puedes usar YAML multi-documento, separado por ---:

# application.yml
app:
  mail:
    host: localhost
---
spring:
  config:
    activate:
      on-profile: prod
app:
  mail:
    host: smtp.shop.vn

El segundo documento solo tiene efecto cuando prod está activo. Este método es más compacto cuando cada profile tiene pocas diferencias. Cuando la configuración es larga, separar en archivos sigue siendo más fácil de leer y revisar.

Nota: En archivos .properties, usa #--- para separar documentos en lugar de ---.

4. Activar profiles

Hay varias formas. El orden de precedencia sigue la regla general de Spring Boot: parámetros de línea de comandos > variables de entorno > archivos de configuración.

Parámetros de línea de comandos:

java -jar shop-service.jar --spring.profiles.active=prod

Propiedades del sistema JVM:

java -Dspring.profiles.active=prod -jar shop-service.jar

Variables de entorno, la forma más común en Docker y Kubernetes:

export SPRING_PROFILES_ACTIVE=prod
java -jar shop-service.jar

Al ejecutar con el plugin Maven:

./mvnw spring-boot:run -Dspring-boot.run.profiles=dev

Mediante código, útil cuando quieres siempre agregar un profile específico:

@SpringBootApplication
public class ShopApplication {

    public static void main(String[] args) {
        SpringApplication app = new SpringApplication(ShopApplication.class);
        app.setAdditionalProfiles("metrics");
        app.run(args);
    }
}

setAdditionalProfiles agrega profiles a la lista, no reemplaza los profiles activados desde fuera. Al ejecutar con --spring.profiles.active=prod, tanto prod como metrics están activos.

Puedes activar múltiples profiles a la vez usando comas: --spring.profiles.active=prod,eu. Si dos archivos definen la misma clave, el profile que viene después gana. En este ejemplo, application-eu.yml sobrescribe application-prod.yml.

Cuando la aplicación inicia, el log te dice qué profiles están activos:

INFO  c.e.shop.ShopApplication : The following 1 profile is active: "prod"

Si no hay profiles activos y tampoco has establecido spring.profiles.default, verás:

INFO  c.e.shop.ShopApplication : No active profile set, falling back to 1 default profile: "default"

Siempre revisa esta línea de log cuando despliegas. Es la forma más rápida de detectar si el servidor está ejecutando el profile equivocado.

5. Habilitar/deshabilitar beans con @Profile

Diferentes configuraciones son solo la mitad del problema. A veces necesitas implementaciones completamente diferentes. Por ejemplo: dev solo imprime emails en la consola, test los guarda en memoria para verificar, prod los envía por SMTP.

Define una interfaz común:

public interface NotificationService {
    void send(String to, String message);
}

Implementación para dev:

@Service
@Profile("dev")
public class ConsoleNotificationService implements NotificationService {

    @Override
    public void send(String to, String message) {
        System.out.println("[DEV] Enviando a " + to + ": " + message);
    }
}

Implementación para test, guarda mensajes para verificaciones:

@Service
@Profile("test")
public class InMemoryNotificationService implements NotificationService {

    private final List<String> sentMessages = new CopyOnWriteArrayList<>();

    @Override
    public void send(String to, String message) {
        sentMessages.add(to + "|" + message);
    }

    public List<String> getSentMessages() {
        return List.copyOf(sentMessages);
    }
}

Implementación para prod:

@Service
@Profile("prod")
public class SmtpNotificationService implements NotificationService {

    private final String host;

    public SmtpNotificationService(@Value("${app.mail.host}") String host) {
        this.host = host;
    }

    @Override
    public void send(String to, String message) {
        // En realidad usarías JavaMailSender; aquí es solo una demostración
        System.out.println("[SMTP " + host + "] Enviando a " + to);
    }
}

La clase que utiliza el servicio no necesita saber en qué entorno se ejecuta:

@Service
public class OrderService {

    private final NotificationService notificationService;

    public OrderService(NotificationService notificationService) {
        this.notificationService = notificationService;
    }

    public void placeOrder(String customerEmail, long orderId) {
        notificationService.send(customerEmail, "Pedido #" + orderId + " ha sido creado");
    }
}

En cualquier momento, solo uno de los tres beans NotificationService se crea. Spring inyecta el bean correcto en OrderService. Al ejecutar con el profile dev y llamar a placeOrder("an@shop.vn", 42), obtienes:

[DEV] Enviando a an@shop.vn: Pedido #42 ha sido creado

5.1. Expresiones de profile

@Profile soporta expresiones lógicas:

@Configuration
public class SecurityConfig {

    @Bean
    @Profile("!prod")
    public CommandLineRunner seedDemoData() {
        return args -> System.out.println("Cargando datos de demostración...");
    }

    @Bean
    @Profile("dev | test")
    public CommandLineRunner relaxedCors() {
        return args -> System.out.println("CORS abierto para cualquier origen");
    }
}
  • !prod: crea el bean en todos los profiles excepto prod.
  • dev | test: crea cuando dev o test está activo. También hay & para “y”, por ejemplo prod & eu.

@Profile se puede poner en clases @Component/@Configuration o en métodos @Bean individuales. Si se coloca en @Configuration, todos los beans dentro están sujetos a esa condición.

5.2. Comprobar el profile en código

A veces necesitas bifurcar la lógica. Usa Environment:

@Component
public class StartupBanner implements CommandLineRunner {

    private final Environment environment;

    public StartupBanner(Environment environment) {
        this.environment = environment;
    }

    @Override
    public void run(String... args) {
        if (environment.acceptsProfiles(Profiles.of("prod"))) {
            System.out.println("Ejecutándose en PRODUCCIÓN");
        }
    }
}

Usa este enfoque con moderación. Si tu código está lleno de if (prod), deberías separar en beans diferentes con @Profile.

6. Grupos de profiles

A medida que el sistema crece, podrías querer dividir la configuración, por ejemplo proddb para base de datos y prodmq para message broker. Los grupos de profiles te permiten activar un conjunto completo con un solo nombre:

# application.yml
spring:
  profiles:
    group:
      prod: proddb, prodmq
      dev: devdb

Ahora ejecutar --spring.profiles.active=prod activa prod, proddb y prodmq. Spring carga secuencialmente application-prod.yml, application-proddb.yml, application-prodmq.yml. El operador solo necesita recordar un nombre.

7. Escribir tests con @ActiveProfiles

En los tests, seleccionas el profile usando @ActiveProfiles. El test siguiente verifica dos cosas: que se inyecta el bean correcto y que la configuración de application-test.yml está en vigor.

@SpringBootTest
@ActiveProfiles("test")
class OrderServiceProfileTest {

    @Autowired
    private OrderService orderService;

    @Autowired
    private NotificationService notificationService;

    @Value("${app.mail.host}")
    private String mailHost;

    @Test
    void shouldUseInMemoryNotificationInTestProfile() {
        assertThat(notificationService).isInstanceOf(InMemoryNotificationService.class);
    }

    @Test
    void shouldRecordNotificationWhenOrderPlaced() {
        orderService.placeOrder("binh@shop.vn", 7);

        var inMemory = (InMemoryNotificationService) notificationService;
        assertThat(inMemory.getSentMessages())
                .contains("binh@shop.vn|Pedido #7 ha sido creado");
    }

    @Test
    void shouldOverrideMailHostFromTestProfile() {
        assertThat(mailHost).isEqualTo("fake-smtp");
    }
}

Explicación:

  • @ActiveProfiles("test") activa el profile test para el contexto de esta clase, independientemente de cuál sea spring.profiles.default.
  • El primer test confirma que Spring elige InMemoryNotificationService en lugar de enviar emails reales.
  • El segundo test usa el bean in-memory para verificar el contenido del mensaje sin necesidad de mock.
  • El tercer test demuestra que app.mail.host de application-test.yml ha sobrescrito el valor localhost en application.yml.

Nota: Cada combinación de profiles diferente crea un contexto de Spring separado. Si cada clase de test usa un conjunto de profiles diferente, la caché de contexto no se puede reutilizar y la suite de tests se ejecutará mucho más lentamente. Mantén el número de combinaciones de profiles en los tests al mínimo.

8. Errores comunes

Establecer spring.profiles.active en un archivo según el profile. Desde Spring Boot 2.4, declarar spring.profiles.active o spring.profiles.include en application-dev.yml (o en un documento con on-profile) hará que la aplicación no se inicie:

InvalidConfigDataPropertyException: Property 'spring.profiles.active' imported from
location 'class path resource [application-dev.yml]' is invalid in a profile specific resource

Usa grupos de profiles (sección 6) en su lugar.

Hardcodear spring.profiles.active=dev en application.yml. Este archivo se empaqueta en el jar. Si alguien olvida pasar la variable de entorno en el servidor, prod se ejecutará con la configuración de dev. Usa spring.profiles.default para el entorno local y siempre activa el profile explícitamente en el servidor.

Dejar secretos en application-prod.yml. Las contraseñas de base de datos o claves API no deben estar en Git. Usa placeholders como ${DB_PASSWORD} y proporciona los valores a través de variables de entorno o un gestor de secretos.

Ningún bean coincide con el profile. Si ejecutas con el profile staging pero solo tienes implementaciones para dev, test, prod, la aplicación reportará un error:

Parameter 0 of constructor in OrderService required a bean of type
'NotificationService' that could not be found.

Considera un bean de respaldo con @Profile("!prod") o agrega una implementación para el nuevo profile.

Abusar de profiles para feature flags. Los profiles son apropiados para describir entornos. Si quieres habilitar/deshabilitar una característica independientemente del entorno, @ConditionalOnProperty es generalmente más claro:

@Bean
@ConditionalOnProperty(name = "app.feature.loyalty-points", havingValue = "true")
public LoyaltyService loyaltyService() {
    return new LoyaltyService();
}

Cuando tienes profiles como prod-with-loyalty o dev-no-cache, es señal de que deberías cambiar a propiedades.

9. Conclusión

Los profiles te ayudan a separar la configuración y el comportamiento según cada entorno sin modificar el código. Has visto cómo usar application-{profile}.yml para sobrescribir configuración, @Profile para elegir implementaciones, grupos de profiles para combinar varios profiles, y @ActiveProfiles para tests.

Algunos principios a recordar: coloca la configuración común en application.yml, escribe solo las diferencias en archivos de profile. Activa profiles en el servidor a través de variables de entorno o parámetros de línea de comandos. No pongas secretos en Git. Y siempre verifica la línea de log The following profiles are active después de cada despliegue.

spring-bootprofileconfigurationtesting