Clean Architecture en Java Spring Boot et Angular : Séparer les préoccupations pour l’évolutivité
La Clean Architecture vise à organiser un système en couches clairement séparées, afin de réduire le couplage, d’améliorer la testabilité et de faciliter l’évolution. Dans un contexte Java Spring Boot côté backend et Angular côté frontend, cette approche permet de gouverner la complexité : les décisions métiers restent indépendantes des frameworks, tandis que l’infrastructure (web, persistance, HTTP) se limite à l’exécution.
Pourquoi séparer les préoccupations ?
Sans séparation stricte, les évolutions (changement de base de données, ajout d’un protocole API, refactor UI, introduction d’un outil d’observabilité) deviennent coûteuses. La Clean Architecture impose une frontière stable entre :
- le domaine (règles métier et invariants)
- les cas d’usage (coordination des actions métier)
- l’adaptateur (contrôleurs HTTP, persistance, services externes)
- le frontend (présentation et expérience utilisateur)
Principes clés de Clean Architecture
1) Dépendances dirigées vers le centre
Les couches internes ne doivent pas dépendre des couches externes. Les implémentations techniques sont fournies en périphérie via des interfaces côté domaine/cas d’usage.
2) Cas d’usage au centre
Les cas d’usage orchestrent les opérations métier sans supposer l’existence d’un framework web ou d’un ORM.
3) Interfaces portées par le domaine
Le domaine déclare les ports (contrats). Les adaptateurs fournissent les adapters (implémentations concrètes).
Architecture recommandée pour Spring Boot (backend)
Une structure fréquemment utilisée pour une application Spring Boot compatible Clean Architecture consiste à isoler les modules (ou packages) par rôle. L’objectif est de rendre la couche domaine/cas d’usage “framework-agnostique”.
Structure de packages (exemple)
com.example
├── application
│ ├── ports
│ │ ├── in
│ │ │ └── ManageCustomerUseCase.java
│ │ └── out
│ │ └── CustomerRepositoryPort.java
│ └── services
│ └── ManageCustomerService.java
├── domain
│ ├── model
│ │ └── Customer.java
│ └── exceptions
│ └── DomainException.java
├── adapter
│ ├── in
│ │ └── http
│ │ └── CustomerController.java
│ └── out
│ └── persistence
│ ├── CustomerEntity.java
│ └── JpaCustomerRepositoryAdapter.java
└── config
└── DependenciesConfig.java
Cas d’usage : interface “port in”
package com.example.application.ports.in;
public interface ManageCustomerUseCase {
CustomerDto createCustomer(CreateCustomerCommand command);
}
Cas d’usage : implémentation “service” (métier)
package com.example.application.services;
import com.example.application.ports.in.ManageCustomerUseCase;
import com.example.application.ports.out.CustomerRepositoryPort;
import com.example.domain.model.Customer;
public class ManageCustomerService implements ManageCustomerUseCase {
private final CustomerRepositoryPort repository;
public ManageCustomerService(CustomerRepositoryPort repository) {
this.repository = repository;
}
@Override
public CustomerDto createCustomer(CreateCustomerCommand command) {
Customer customer = Customer.create(command.name(), command.email());
Customer saved = repository.save(customer);
return CustomerDto.from(saved);
}
}
Port “out” : abstraction de la persistance
package com.example.application.ports.out;
import com.example.domain.model.Customer;
public interface CustomerRepositoryPort {
Customer save(Customer customer);
}
Adapter HTTP : contrôleur Spring Boot
Le contrôleur traduit uniquement des requêtes HTTP en commandes applicatives et formate les réponses. La logique métier ne doit pas vivre ici.
package com.example.adapter.in.http;
import com.example.application.ports.in.ManageCustomerUseCase;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/customers")
public class CustomerController {
private final ManageCustomerUseCase useCase;
public CustomerController(ManageCustomerUseCase useCase) {
this.useCase = useCase;
}
@PostMapping
public CustomerDto create(@RequestBody CreateCustomerCommand command) {
return useCase.createCustomer(command);
}
}
Adapter persistance : implémentation JPA
L’adapter utilise un ORM (ici JPA) pour convertir entités et modèles domaine. L’implémentation technique reste à l’extérieur du cœur métier.
package com.example.adapter.out.persistence;
import com.example.application.ports.out.CustomerRepositoryPort;
import com.example.domain.model.Customer;
public class JpaCustomerRepositoryAdapter implements CustomerRepositoryPort {
private final JpaSpringCustomerRepository jpaRepo;
public JpaCustomerRepositoryAdapter(JpaSpringCustomerRepository jpaRepo) {
this.jpaRepo = jpaRepo;
}
@Override
public Customer save(Customer customer) {
CustomerEntity entity = CustomerEntity.from(customer);
CustomerEntity saved = jpaRepo.save(entity);
return saved.toDomain();
}
}
Frontend Angular : séparation présentation et logique métier
Angular peut aussi appliquer la séparation des préoccupations en distinguant : une couche de présentation, des services d’accès API, et une logique métier UI (optionnelle) qui ne doit pas polluer les modèles applicatifs.
Organisation front typique
src/app
├── core
│ ├── api
│ │ └── customer-api.service.ts
│ └── models
│ └── customer.models.ts
├── features
│ └── customers
│ ├── pages
│ │ └── customer-create.page.ts
│ └── state
│ └── customer-create.facade.ts
└── shared
└── ui
└── form-components/
Service API : adapter vers le backend
// customer-api.service.ts
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
export interface CreateCustomerCommand {
name: string;
email: string;
}
export interface CustomerDto {
id: string;
name: string;
email: string;
}
@Injectable({ providedIn: 'root' })
export class CustomerApiService {
constructor(private http: HttpClient) {}
createCustomer(command: CreateCustomerCommand): Observable<CustomerDto> {
return this.http.post<CustomerDto>('/api/customers', command);
}
}
Facade : coordination côté UI
// customer-create.facade.ts
import { Injectable } from '@angular/core';
import { CustomerApiService, CreateCustomerCommand, CustomerDto } from '../../core/api/customer-api.service';
import { Observable } from 'rxjs';
@Injectable()
export class CustomerCreateFacade {
constructor(private api: CustomerApiService) {}
submit(command: CreateCustomerCommand): Observable<CustomerDto> {
return this.api.createCustomer(command);
}
}
Page : uniquement la présentation
// customer-create.page.ts (extrait)
import { Component } from '@angular/core';
import { CustomerCreateFacade } from './state/customer-create.facade';
@Component({
selector: 'app-customer-create',
templateUrl: './customer-create.page.html'
})
export class CustomerCreatePage {
constructor(private facade: CustomerCreateFacade) {}
// La page appelle le facade et gère l’état de formulaire/affichage.
}
Gestion des erreurs et contrats : réduire l’effet domino
Une évolution backend ne devrait pas forcer un refactor massif frontend. La Clean Architecture encourage des contrats explicites et des erreurs normalisées.
Exemple : mapper erreurs métier vers HTTP
@RestControllerAdvice
public class ApiExceptionHandler {
@ExceptionHandler(DomainException.class)
public ResponseEntity<ApiError> handleDomain(DomainException ex) {
return ResponseEntity.badRequest()
.body(new ApiError("DOMAIN_ERROR", ex.getMessage()));
}
}
Bénéfices concrets pour l’évolutivité
- Remplacement facile des infrastructures (JPA → autre persistance, ou REST → gRPC) sans toucher le cœur métier.
- Testabilité accrue : les cas d’usage peuvent être testés sans démarrer Spring ni appeler une base.
- Propagation des changements maîtrisée : les adaptateurs isolent les détails techniques.
- Évolution itérative : l’ajout de fonctionnalités s’intègre via de nouveaux cas d’usage/ports.
Bonnes pratiques d’implémentation
Éviter l’injection “au mauvais niveau”
Les dépendances techniques doivent être branchées via des config/adapters. Les services de cas d’usage restent centrés sur le métier.
Préférer des DTOs dédiés au contexte
Le domaine peut exposer des entités, mais les APIs peuvent utiliser des DTOs adaptés au contrat HTTP. Cette distinction limite les ruptures en cas d’évolution du modèle.
Documenter les ports et invariants métier
La maintenance devient plus simple quand les invariants sont explicites, et quand les ports décrivent clairement les exigences des cas d’usage.
Conclusion
La Clean Architecture, appliquée conjointement à Spring Boot et Angular, permet de bâtir des systèmes où la logique métier demeure stable. Les adaptateurs s’occupent de l’infrastructure, la présentation s’occupe de l’expérience utilisateur, et les cas d’usage orchestrent les règles métier. Cette séparation des préoccupations constitue un levier direct pour l’évolutivité, la qualité et la résilience logicielle.
À propos de l'auteur
Laty Gueye Samba est développeur Full Stack basé à Dakar, Sénégal. Spécialiste des écosystèmes Java / Spring Boot et Angular.
Contact : latygueyesamba@gmail.com | Dakar, Sénégal