Skip to main content

TP5.3 - Profile Configuration

Objectives​

By the end of this lab, you will be able to:

  1. ✅ Create profiles for dev, test, and prod
  2. ✅ Create conditional Beans according to the profile
  3. ✅ Use application-\{profile\}.properties files
  4. ✅ Activate a profile and test

Estimated duration: 1h30


Prerequisites​

  • ✅ Chapter 4 of Module 5 completed
  • ✅ TP5.1 and TP5.2 completed
  • ✅ Maven 3.9+ and Java 17+

Preparation: New Project​

Create the Project​

Via Spring Initializr:

  • Project: Maven
  • Spring Boot: 3.2.0
  • Artifact: profiles-demo
  • Dependencies: Spring Web, Spring Data JPA, H2 Database, PostgreSQL Driver

Structure:

src/main/java/com/example/profiles/
├── ProfilesDemoApplication.java
├── service/
│ ├── EmailService.java
│ ├── FakeEmailService.java
│ └── SmtpEmailService.java
├── config/
│ ├── DevConfig.java
│ ├── ProdConfig.java
│ └── DatabaseConfig.java
└── controller/
└── TestController.java

src/main/resources/
├── application.properties
├── application-dev.properties
├── application-test.properties
└── application-prod.properties

Exercise 1: Profiles for Email Services​

Objective​

Create 2 implementations of EmailService: a fake one for dev, a real one for prod.

Step 1.1: Create the Interface​

File: service/EmailService.java

package com.example.profiles.service;

public interface EmailService {
void send(String to, String subject, String body);
}

Step 1.2: DEV Implementation (Fake)​

File: service/FakeEmailService.java

package com.example.profiles.service;

import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;

@Service
@Profile("dev") // ← Active ONLY in dev
public class FakeEmailService implements EmailService {

@Override
public void send(String to, String subject, String body) {
System.out.println("========== FAKE EMAIL ==========");
System.out.println("To: " + to);
System.out.println("Subject: " + subject);
System.out.println("Body: " + body);
System.out.println("================================");
System.out.println("(Email simulated - not actually sent)");
}
}

Key Points:

  • @Profile("dev") → Bean created only in dev profile
  • Simulated email → No real sending

Step 1.3: PROD Implementation (Real)​

File: service/SmtpEmailService.java

package com.example.profiles.service;

import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;

@Service
@Profile("prod") // ← Active ONLY in prod
public class SmtpEmailService implements EmailService {

@Override
public void send(String to, String subject, String body) {
System.out.println("========== SMTP EMAIL ==========");
System.out.println("Connecting to SMTP server...");
System.out.println("Authenticating...");
System.out.println("Sending to: " + to);
System.out.println("Subject: " + subject);
System.out.println("================================");
System.out.println("(Email actually sent)");

// In production, here you would use JavaMail:
// Session session = Session.getInstance(properties, authenticator);
// MimeMessage message = new MimeMessage(session);
// Transport.send(message);
}
}

Step 1.4: Create the Test Controller​

File: controller/TestController.java

package com.example.profiles.controller;

import com.example.profiles.service.EmailService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/test")
public class TestController {

private final EmailService emailService;

@Value("${spring.profiles.active:default}")
private String activeProfile;

public TestController(EmailService emailService) {
this.emailService = emailService;
}

@GetMapping("/email")
public String testEmail() {
emailService.send(
"user@example.com",
"Test Email",
"This is a test email"
);

return "✅ Email sent via profile: " + activeProfile +
" - See console";
}

@GetMapping("/profile")
public String getActiveProfile() {
return "Active profile: " + activeProfile;
}
}

Step 1.5: Activate the DEV Profile​

File: src/main/resources/application.properties

# Active profile
spring.profiles.active=dev

# Application name
spring.application.name=Profiles Demo

Step 1.6: Test in DEV​

Start:

mvn clean install -DskipTests spring-boot:run

Console on startup:

The following 1 profile is active: "dev"

Test:

http://localhost:8080/api/test/email

Console:

========== FAKE EMAIL ==========
To: user@example.com
Subject: Test Email
Body: This is a test email
================================
(Email simulated - not actually sent)

✅ In DEV: Simulated email!


Step 1.7: Test in PROD​

Modify application.properties:

spring.profiles.active=prod

Restart and test:

Console:

The following 1 profile is active: "prod"

========== SMTP EMAIL ==========
Connecting to SMTP server...
Authenticating...
Sending to: user@example.com
Subject: Test Email
================================
(Email actually sent)

✅ In PROD: Real email!


Exercise 2: Database Configuration by Profile​

Objective​

Use H2 in dev/test, PostgreSQL in prod.

Step 2.1: Common application.properties File​

File: src/main/resources/application.properties

# Active profile (change as needed)
spring.profiles.active=dev

# Common configuration
spring.application.name=Profiles Demo
server.port=8080

# Common JPA
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

Step 2.2: DEV Configuration (H2)​

File: src/main/resources/application-dev.properties

# In-memory H2 database
spring.datasource.url=jdbc:h2:mem:devdb
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=

# H2 Console
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console

# Hibernate
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect

# Logs
logging.level.root=DEBUG
logging.level.com.example.profiles=DEBUG

Step 2.3: TEST Configuration (H2 with scripts)​

File: src/main/resources/application-test.properties

# In-memory H2 database for tests
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=

# Hibernate
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect

# Minimal logs for tests
logging.level.root=WARN
logging.level.com.example.profiles=INFO

Step 2.4: PROD Configuration (PostgreSQL)​

File: src/main/resources/application-prod.properties

# PostgreSQL database
spring.datasource.url=jdbc:postgresql://localhost:5432/proddb
spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.username=admin
spring.datasource.password=admin123

# Hibernate
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect

# Minimal logs
logging.level.root=WARN
logging.level.com.example.profiles=INFO

# Security (in production, use environment variables!)
# spring.datasource.username=${DB_USERNAME}
# spring.datasource.password=${DB_PASSWORD}

Step 2.5: Create a Test Entity​

File: model/User.java

package com.example.profiles.model;

import jakarta.persistence.*;

@Entity
@Table(name = "users")
public class User {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String name;
private String email;

// Constructors
public User() {}

public User(String name, String email) {
this.name = name;
this.email = email;
}

// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }

public String getName() { return name; }
public void setName(String name) { this.name = name; }

public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
}

Step 2.6: Test Different DBs​

In DEV (H2):

spring.profiles.active=dev

Start and check the console:

H2 console available at '/h2-console'
Hibernate: create table users (...)
Database: H2

Access the H2 console:

http://localhost:8080/h2-console
JDBC URL: jdbc:h2:mem:devdb
Username: sa
Password: (empty)

In PROD (PostgreSQL):

  1. First create the database:
CREATE DATABASE proddb;
  1. Modify the profile:
spring.profiles.active=prod
  1. Start:
Database: PostgreSQL
Hibernate: validate schema

Exercise 3: Configuration Beans by Profile​

Objective​

Create different Beans depending on the environment.

Step 3.1: DEV Configuration​

File: config/DevConfig.java

package com.example.profiles.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

@Configuration
@Profile("dev") // ← Entire class active in dev
public class DevConfig {

@Bean
public String environmentName() {
return "DEVELOPMENT";
}

@Bean
public boolean debugMode() {
return true;
}

@Bean
public String apiUrl() {
return "http://localhost:8080/api";
}
}

Step 3.2: PROD Configuration​

File: config/ProdConfig.java

package com.example.profiles.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

@Configuration
@Profile("prod") // ← Entire class active in prod
public class ProdConfig {

@Bean
public String environmentName() {
return "PRODUCTION";
}

@Bean
public boolean debugMode() {
return false;
}

@Bean
public String apiUrl() {
return "https://api.mysite.com/api";
}
}

Step 3.3: Test​

Add in TestController.java:

@RestController
@RequestMapping("/api/test")
public class TestController {

private final EmailService emailService;
private final String environmentName;
private final boolean debugMode;
private final String apiUrl;

@Value("${spring.profiles.active:default}")
private String activeProfile;

public TestController(
EmailService emailService,
String environmentName,
boolean debugMode,
String apiUrl
) {
this.emailService = emailService;
this.environmentName = environmentName;
this.debugMode = debugMode;
this.apiUrl = apiUrl;
}

// ... other methods ...

@GetMapping("/config")
public String getConfig() {
return String.format(
"Environment: %s<br>" +
"Active profile: %s<br>" +
"Debug mode: %s<br>" +
"API URL: %s",
environmentName,
activeProfile,
debugMode,
apiUrl
);
}
}

In DEV:

Environment: DEVELOPMENT
Active profile: dev
Debug mode: true
API URL: http://localhost:8080/api

In PROD:

Environment: PRODUCTION
Active profile: prod
Debug mode: false
API URL: https://api.mysite.com/api

Exercise 4: Multiple Profiles and Composite @Profile​

Objective​

Activate multiple profiles and create complex conditions.

Step 4.1: Bean for Multiple Profiles​

Add in config/DevConfig.java:

@Bean
@Profile({"dev", "test"}) // ← Active in dev OR test
public String sharedDevTestBean() {
return "Bean shared between dev and test";
}

Step 4.2: Complex Conditional Bean​

@Bean
@Profile("!prod") // ← Active in ALL profiles EXCEPT prod
public String nonProdBean() {
return "Bean for non-production (dev, test, etc.)";
}

Step 4.3: Activate Multiple Profiles​

Method 1: application.properties

spring.profiles.active=dev,mock-data,debug

Method 2: Command line

mvn spring-boot:run -Dspring-boot.run.profiles=dev,mock-data

Method 3: Environment variable

export SPRING_PROFILES_ACTIVE=prod,monitoring
mvn spring-boot:run

Exercise 5: Complete Multi-Environment Configuration​

Objective​

Create a production-ready configuration.

Step 5.1: Environment Variables in PROD​

File: application-prod.properties

# Database (from environment variables)
spring.datasource.url=${DATABASE_URL:jdbc:postgresql://localhost:5432/proddb}
spring.datasource.username=${DB_USERNAME:admin}
spring.datasource.password=${DB_PASSWORD}

# SMTP (from environment variables)
spring.mail.host=${SMTP_HOST:smtp.gmail.com}
spring.mail.port=${SMTP_PORT:587}
spring.mail.username=${SMTP_USERNAME}
spring.mail.password=${SMTP_PASSWORD}

# API Keys
app.external-api.key=${EXTERNAL_API_KEY}
app.external-api.url=${EXTERNAL_API_URL:https://api.service.com}

Syntax:

  • $\{VAR_NAME\} → Mandatory variable (error if absent)
  • ${VAR_NAME:default} → Variable with default value

Step 5.2: Deploy with Variables​

Linux/Mac:

export DATABASE_URL="jdbc:postgresql://prod-server:5432/myapp"
export DB_USERNAME="app_user"
export DB_PASSWORD="secret123"
export SMTP_USERNAME="noreply@myapp.com"
export SMTP_PASSWORD="smtp_secret"

mvn spring-boot:run

Windows:

$env:DATABASE_URL="jdbc:postgresql://prod-server:5432/myapp"
$env:DB_USERNAME="app_user"
# ...

mvn spring-boot:run

Recap​

File Structure​

application.properties          ← Common configuration
application-dev.properties ← Dev specific
application-test.properties ← Test specific
application-prod.properties ← Prod specific

Priority order:

1. application-{profile}.properties (specific)
2. application.properties (common)
3. Environment variables
4. Command line arguments

Beans by Profile​

// Bean for ONE profile
@Profile("dev")

// Bean for MULTIPLE profiles
@Profile({"dev", "test"})

// Bean for ALL except one
@Profile("!prod")

// Entire class
@Configuration
@Profile("dev")
public class DevConfig { }

Activate a Profile​

1. application.properties:

spring.profiles.active=dev

2. Command line:

--spring.profiles.active=prod

3. Environment variable:

export SPRING_PROFILES_ACTIVE=prod

4. IDE (VS Code):

"spring.profiles.active": "dev"

Self-Assessment​

  1. What is the difference between application.properties and application-dev.properties?
  2. How to create a Bean active only in dev?
  3. How to activate multiple profiles simultaneously?
  4. Why use environment variables in production?
  5. What is the syntax for a variable with a default value?

Bonus Challenge 🎯​

Create a complete configuration for a real application:

  1. 3 profiles: dev, test, prod

  2. Dev:

    • In-memory H2
    • Fake email
    • DEBUG logs
    • No security
  3. Test:

    • H2 with scripts
    • Mock email
    • INFO logs
  4. Prod:

    • PostgreSQL
    • Real SMTP email
    • WARN logs
    • Security enabled
    • Environment variables

Bonus: Add a docker profile for Docker Compose


Congratulations! 🎉​

You have completed Module 5!

You now master:

  • ✅ Inversion of Control (IoC)
  • ✅ Dependency Injection (DI)
  • ✅ Beans and their lifecycle
  • ✅ Scopes (Singleton, Prototype)
  • ✅ Profile configuration
  • ✅ Multi-environments (dev/test/prod)

You are ready for advanced concepts!


⬅️ Previous Lab: Beans and Scopes

⬅️ Back to Module 5

➡️ Next Module: Advanced Spring Data JPA