Couche Infrastructure
La couche Infrastructure contient les adaptateurs qui implémentent les détails techniques.
L’infrastructure est la couche la plus externe : elle fait le lien entre votre application et le monde extérieur (BDD, APIs, fichiers).
Rôle de la Couche Infrastructure
La couche Infrastructure a 3 responsabilités principales :
Implémentation
1. Implémenter les Ports de Sortie
L’infrastructure implémente les interfaces définies par le domain.
// domain/port/output/TrendRepository.java (interface)
public interface TrendRepository {
Optional<TrendResult> getTrends(TrendQuery query);
}
// infrastructure/adapter/MockTrendRepositoryAdapter.java (implémentation)
@Singleton
public class MockTrendRepositoryAdapter implements TrendRepository {
@Override
public Optional<TrendResult> getTrends(TrendQuery query) {
return Optional.of(generateMockData());
}
}Structure de la Couche Infrastructure
- MockTrendRepositoryAdapter.java
- GoogleTrendsApiAdapter.java
- CountryRepositoryAdapter.java
Organisation :
adapter/= Implémentations des ports de sortiedto/= DTOs techniques (API externe, BDD)config/= Configuration (HttpClient, ObjectMapper)
Types d’Adapters
1. Mock Adapter (Développement)
Utilisé pour le développement sans dépendre d’API externe.
@Singleton
@Requires(property = "app.trend-source", value = "mock")
public class MockTrendRepositoryAdapter implements TrendRepository {
@Override
public Optional<TrendResult> getTrends(TrendQuery query) {
// Générer des données mockées
TrendResult result = TrendResult.builder()
.keyword(query.getKeyword())
.region(query.getRegion())
.interestScore(generateRandomScore())
.relatedTopics(List.of())
.queriedAt(LocalDateTime.now())
.build();
return Optional.of(result);
}
private int generateRandomScore() {
return ThreadLocalRandom.current().nextInt(50, 101);
}
}@Requires permet de choisir l’adapter selon la configuration :
# application-dev.yml
app:
trend-source: mock
# application-prod.yml
app:
trend-source: google-api2. API Adapter (HTTP Client)
Appelle une API externe REST.
@Slf4j
@Singleton
@Requires(property = "app.trend-source", value = "google-api")
public class GoogleTrendsApiAdapter implements TrendRepository {
@Inject
@Client(id = "google-trends", path = "/trends")
HttpClient httpClient;
@Override
public Optional<TrendResult> getTrends(TrendQuery query) {
try {
log.info("Calling Google Trends API for keyword={}", query.getKeyword());
// 1. Appeler l'API (Jackson désérialise automatiquement)
String url = String.format(
"/explore?keyword=%s®ion=%s",
query.getKeyword(),
query.getRegion()
);
GoogleApiDto dto = httpClient.toBlocking()
.retrieve(HttpRequest.GET(url), GoogleApiDto.class);
// 2. Convertir DTO → Domain
TrendResult result = toDomain(dto, query);
log.info("Got trend score={}", result.getInterestScore());
return Optional.of(result);
} catch (HttpClientException e) {
log.error("Failed to call Google Trends API", e);
return Optional.empty();
}
}
private TrendResult toDomain(GoogleApiDto dto, TrendQuery query) {
return TrendResult.builder()
.keyword(query.getKeyword())
.region(query.getRegion())
.interestScore(dto.getScore())
.relatedTopics(convertRelatedTopics(dto.getRelated()))
.queriedAt(LocalDateTime.now())
.build();
}
private List<RelatedTopic> convertRelatedTopics(List<GoogleRelatedDto> dtos) {
if (dtos == null) {
return List.of();
}
return dtos.stream()
.map(dto -> new RelatedTopic(dto.getTopic(), dto.getValue()))
.collect(Collectors.toList());
}
}3. Database Adapter (JPA/JDBC)
Accède à une base de données.
@Slf4j
@Singleton
public class JpaUserRepositoryAdapter implements UserRepository {
@Inject
EntityManager entityManager;
@Override
public Optional<User> findById(Long id) {
UserEntity entity = entityManager.find(UserEntity.class, id);
if (entity == null) {
return Optional.empty();
}
return Optional.of(toDomain(entity));
}
@Override
@Transactional
public void save(User user) {
UserEntity entity = fromDomain(user);
entityManager.persist(entity);
}
private User toDomain(UserEntity entity) {
return new User(
entity.getId(),
entity.getName(),
new Email(entity.getEmail()),
entity.getCreatedAt()
);
}
private UserEntity fromDomain(User user) {
UserEntity entity = new UserEntity();
entity.setId(user.getId());
entity.setName(user.getName());
entity.setEmail(user.getEmail().getValue());
entity.setCreatedAt(user.getCreatedAt());
return entity;
}
}4. File Adapter
Lit/Écrit des fichiers.
@Slf4j
@Singleton
public class FileConfigurationAdapter implements ConfigurationRepository {
private final ObjectMapper objectMapper;
private final String configPath;
@Inject
public FileConfigurationAdapter(
ObjectMapper objectMapper,
@Value("${app.config.path}") String configPath
) {
this.objectMapper = objectMapper;
this.configPath = configPath;
}
@Override
public Optional<Configuration> load() {
try {
File file = new File(configPath);
if (!file.exists()) {
log.warn("Configuration file not found: {}", configPath);
return Optional.empty();
}
ConfigurationDto dto = objectMapper.readValue(file, ConfigurationDto.class);
return Optional.of(toDomain(dto));
} catch (IOException e) {
log.error("Failed to load configuration", e);
return Optional.empty();
}
}
@Override
public void save(Configuration config) {
try {
ConfigurationDto dto = fromDomain(config);
objectMapper.writeValue(new File(configPath), dto);
log.info("Configuration saved to {}", configPath);
} catch (IOException e) {
log.error("Failed to save configuration", e);
throw new ConfigurationException("Failed to save", e);
}
}
}DTOs Infrastructure
Les DTOs Infrastructure mappent les structures techniques (JSON API, BDD).
DTO pour API Externe
@Data
@NoArgsConstructor
public class GoogleApiDto {
@JsonProperty("keyword")
private String keyword;
@JsonProperty("interest_score")
private Integer score;
@JsonProperty("related_queries")
private List<GoogleRelatedDto> related;
}
@Data
@NoArgsConstructor
public class GoogleRelatedDto {
@JsonProperty("query")
private String topic;
@JsonProperty("value")
private Integer value;
}Entity JPA (pour BDD)
@Entity
@Table(name = "users")
@Data
public class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = false, unique = true)
private String email;
@Column(name = "created_at")
private LocalDateTime createdAt;
}Conversion DTO ↔ Domain
La conversion DOIT être dans l’adaptateur, PAS dans le DTO ou le domain.
Exemple Complet
@Slf4j
@Singleton
public class CountryRepositoryAdapter implements CountryRepository {
@Inject
@Client(id = "countries", path = "/v3.1")
HttpClient httpClient;
@Override
public List<Country> findAll() {
try {
// 1. Appeler l'API → DTO (Jackson automatique)
CountryApiDto[] dtos = httpClient.toBlocking()
.retrieve("/all?fields=name,translations", CountryApiDto[].class);
// 2. Convertir DTO → Domain
return Stream.of(dtos)
.map(this::toDomain)
.collect(Collectors.toList());
} catch (HttpClientException e) {
log.error("Failed to fetch countries", e);
return List.of();
}
}
/**
* Convertit CountryApiDto (infrastructure) → Country (domain).
*/
private Country toDomain(CountryApiDto dto) {
Name name = new Name(
dto.getName().getCommon(),
dto.getName().getOfficial()
);
List<Translation> translations = convertTranslations(
dto.getTranslations()
);
return new Country(name, translations);
}
/**
* Convertit Map<String, TranslationDto> → List<Translation>.
*/
private List<Translation> convertTranslations(
Map<String, TranslationDto> translationsMap
) {
if (translationsMap == null) {
return List.of();
}
return translationsMap.entrySet().stream()
.map(entry -> new Translation(
entry.getKey(),
entry.getValue().getCommon(),
entry.getValue().getOfficial()
))
.collect(Collectors.toList());
}
}Configuration
HttpClient Configuration
# application.yml
micronaut:
application:
name: google-trends-app
server:
port: 8080
http:
client:
read-timeout: 30s
services:
google-trends:
urls:
- https://trends.google.com
read-timeout: 30s
countries:
urls:
- https://restcountries.com
read-timeout: 10sObjectMapper Factory
@Factory
public class ObjectMapperFactory {
@Bean
@Named("logging")
@Singleton
public ObjectMapper loggingObjectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
return mapper;
}
}Choisir l’Adapter avec @Requires
Utilisez @Requires pour basculer entre adapters selon l’environnement.
Mock pour dev
@Singleton
@Requires(property = "app.trend-source", value = "mock")
public class MockTrendRepositoryAdapter implements TrendRepository {
// ...
}API pour prod
@Singleton
@Requires(property = "app.trend-source", value = "google-api")
public class GoogleTrendsApiAdapter implements TrendRepository {
// ...
}Configuration
# application-dev.yml
app:
trend-source: mock
# application-prod.yml
app:
trend-source: google-apiMicronaut injecte automatiquement le bon adapter selon la configuration !
Gestion des Erreurs
Try-Catch dans l’Adapter
@Override
public Optional<TrendResult> getTrends(TrendQuery query) {
try {
GoogleApiDto dto = httpClient.toBlocking()
.retrieve(buildUrl(query), GoogleApiDto.class);
return Optional.of(toDomain(dto, query));
} catch (HttpClientException e) {
log.error("API call failed for keyword={}", query.getKeyword(), e);
return Optional.empty();
}
}Retry avec @Retryable
@Retryable(
attempts = "3",
delay = "1s",
multiplier = "2.0"
)
@Override
public Optional<TrendResult> getTrends(TrendQuery query) {
// Retry automatique en cas d'erreur
GoogleApiDto dto = httpClient.toBlocking()
.retrieve(buildUrl(query), GoogleApiDto.class);
return Optional.of(toDomain(dto, query));
}Bonnes Pratiques
1. Un Adapter = Un Port
// ✅ BON : Un adapter par port
@Singleton
public class MockTrendRepositoryAdapter implements TrendRepository {
// ...
}
// ❌ MAUVAIS : Un adapter pour plusieurs ports
@Singleton
public class MockAdapter implements TrendRepository, UserRepository {
// Trop de responsabilités
}2. Logs dans l’Adapter
@Override
public Optional<TrendResult> getTrends(TrendQuery query) {
log.info("Calling API for keyword={}, region={}",
query.getKeyword(), query.getRegion());
TrendResult result = // ... API call
log.info("Got score={} for keyword={}",
result.getInterestScore(), query.getKeyword());
return Optional.of(result);
}3. Méthodes de Conversion Privées
// ✅ BON : Méthodes privées dans l'adapter
private TrendResult toDomain(GoogleApiDto dto) { }
private GoogleApiDto fromDomain(TrendResult result) { }
// ❌ MAUVAIS : Méthodes publiques dans le DTO
// public class GoogleApiDto {
// public TrendResult toDomain() { } // DTO ne doit pas connaître le domain
// }Checklist Couche Infrastructure
Pour vérifier que votre infrastructure est bien conçue :
- Les adapters implémentent des interfaces du domain
- Les adapters ont des logs (info, error)
- La conversion DTO → Domain est dans l’adapter
- Les DTOs sont dans
infrastructure/dto/ - Les DTOs utilisent
@JsonProperty(Jackson) - Les erreurs sont gérées (try-catch ou @Retryable)
- Utilisation de
@Requirespour choisir l’adapter - Configuration HTTP dans
application.yml
Résumé
L’infrastructure implémente les ports de sortie et gère tous les détails techniques.
| Type d’Adapter | Technologie | Exemple |
|---|---|---|
| Mock | Données en dur | MockTrendRepositoryAdapter |
| API REST | HttpClient | GoogleTrendsApiAdapter |
| Base de données | JPA, JDBC | JpaUserRepositoryAdapter |
| Fichiers | ObjectMapper | FileConfigurationAdapter |
| Cache | Redis, Hazelcast | RedisCacheAdapter |
Principe clé : L’infrastructure traduit entre le monde technique et le domain pur.
Prochaines étapes
Maintenant que vous maîtrisez les 3 couches, explorez comment implémenter concrètement vos adapters.