Introduction
Ce billet explique comment appliquer la Clean Architecture dans un projet combinant Spring Boot pour le backend et Angular pour le frontend. L'objectif est de séparer clairement les responsabilités, protéger le domaine métier des détails techniques et faciliter les tests, l'évolutivité et la maintenance. L'approche présentée s'appuie sur les couches Domain, Use Cases (Application), Adapters et Infrastructure, ainsi que sur une couche Presentation pour Angular.
Principes essentiels de la Clean Architecture
Indépendance vis-à-vis des frameworks : le cœur métier ne dépend d'aucun framework.
Testabilité : les cas d'utilisation et les entités doivent être testables isolément.
Indépendance de l'UI : l'interface utilisateur peut être changée sans impacter le domaine.
Règle de dépendance : les dépendances pointent vers l'intérieur — les couches externes dépendent des couches internes, jamais l'inverse.
Architecture recommandée
Une structure de projet backend typique pourrait ressembler à :
com.example.project
├─ domain
│ ├─ model
│ └─ port
├─ application
│ └─ usecase
├─ adapter
│ ├─ inbound (web/controllers)
│ └─ outbound (persistence)
└─ infrastructure
└─ configuration
La couche frontend Angular sépare également le modèle métier des services d'accès aux API :
src/app
├─ core (models, ports)
├─ features (components, containers)
└─ adapters (api services, mappers)
Exemple de Domain (Java)
Les entités et interfaces qui représentent le domaine métier restent purs de toute dépendance Spring :
package com.example.project.domain.model;
public class Customer {
private String id;
private String name;
// getters, constructors, equals/hashCode
}
package com.example.project.domain.port;
public interface CustomerRepositoryPort {
Optional findById(String id);
Customer save(Customer customer);
}
Use Case / Application (Java)
Les cas d'utilisation orchestrent la logique métier en s'appuyant sur des ports (interfaces) :
package com.example.project.application.usecase;
import com.example.project.domain.model.Customer;
import com.example.project.domain.port.CustomerRepositoryPort;
public class CreateCustomerUseCase {
private final CustomerRepositoryPort repository;
public CreateCustomerUseCase(CustomerRepositoryPort repository) {
this.repository = repository;
}
public Customer execute(CreateCustomerCommand cmd) {
Customer customer = new Customer(/* mapping from cmd */);
return repository.save(customer);
}
}
Adapter et Infrastructure (Spring Boot)
Les adaptateurs implémentent les ports et utilisent Spring Data ou JDBC. La configuration des beans se situe dans l'infrastructure afin d'injecter les implémentations dans les use cases :
package com.example.project.adapter.out.persistence;
@Repository
public class CustomerRepositoryAdapter implements CustomerRepositoryPort {
private final SpringDataCustomerRepository repo;
public CustomerRepositoryAdapter(SpringDataCustomerRepository repo) {
this.repo = repo;
}
public Optional findById(String id) {
return repo.findById(id).map(entity -> entity.toDomain());
}
}
La configuration lie l'implémentation au port :
@Configuration
public class UseCaseConfig {
@Bean
public CreateCustomerUseCase createCustomerUseCase(CustomerRepositoryPort repo) {
return new CreateCustomerUseCase(repo);
}
}
Controller REST (Adapter inbound)
Les contrôleurs exposent les APIs et appellent les use cases. Ils sont responsables des DTOs et du mapping vers le domaine :
@RestController
@RequestMapping("/customers")
public class CustomerController {
private final CreateCustomerUseCase createCustomerUseCase;
public CustomerController(CreateCustomerUseCase useCase) {
this.createCustomerUseCase = useCase;
}
@PostMapping
public ResponseEntity create(@RequestBody CreateCustomerDto dto) {
Customer customer = createCustomerUseCase.execute(dto.toCommand());
return ResponseEntity.status(HttpStatus.CREATED).body(CustomerDto.fromDomain(customer));
}
}
Structuration côté Angular
Le frontend doit respecter la même séparation : les modèles métiers (interfaces) et les cas d'utilisation (services) n'importent pas directement des détails HTTP. Un adaptateur API réalise le mapping entre les DTOs JSON et les modèles métier.
Exemple TypeScript
export interface Customer {
id: string;
name: string;
}
@Injectable({ providedIn: 'root' })
export class CustomerApiAdapter {
constructor(private http: HttpClient) {}
create(dto: CreateCustomerDto): Observable {
return this.http.post('/api/customers', dto)
.pipe(map(d => ({ id: d.id, name: d.name })));
}
}
Tests et automatisation
Les tests unitaires ciblent les use cases et les entités sans charger Spring. Les tests d'intégration peuvent démarrer Spring et vérifier l'intégration des adapters persistence. Pour Angular, les services métiers se testent avec des mocks pour l'adapter API et les tests E2E vérifient le flux complet.
Bonnes pratiques et pièges à éviter
Respecter la règle de dépendance : éviter d'importer des classes d'infrastructure dans le domaine.
Mapper explicitement : utiliser des mappers entre DTOs et modèles métier plutôt que des conversions ad hoc.
Garder les use cases fins : chaque use case doit correspondre à une action métier cohérente.
Éviter l'anémie du domaine : privilégier des entités riches quand la logique y a sa place, et placer l'orchestration dans les use cases.
Conclusion
Appliquer la Clean Architecture dans un projet Spring Boot + Angular apporte une meilleure séparation des responsabilités, une testabilité accrue et une maintenance facilitée. En définissant clairement les ports et adaptateurs, en isolant le domaine et en centralisant la configuration dans l'infrastructure, l'équipe obtient une base solide pour l'évolution du produit.
À 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