Retour aux articles

Appliquer la Clean Architecture à une application d'entreprise Spring Boot et Angular

Appliquer la Clean Architecture à une application d'entreprise Spring Boot et Angular | Laty Gueye Samba - Développeur Full Stack Dakar Sénégal, Expert Java Spring Boot Angular
```html Appliquer la Clean Architecture à une application d'entreprise Spring Boot et Angular

Appliquer la Clean Architecture à une application d’entreprise avec Spring Boot et Angular

Une application d’entreprise combine souvent des exigences élevées en matière de maintenabilité, de testabilité et de capacité d’évolution. La Clean Architecture permet d’organiser le code autour de la séparation des responsabilités : règles métier au centre, dépendances contrôlées vers l’extérieur, et infrastructure isolée du domaine.

Objectifs et bénéfices attendus

  • Indépendance du framework : le domaine ne dépend ni de Spring ni d’Angular.
  • Testabilité accrue : les use cases sont testés sans accès à la base ou au réseau.
  • Évolutivité : l’UI, la persistance, et les intégrations évoluent sans impacter les règles métier.
  • Contrôle des dépendances : la direction des appels est maîtrisée.

Principes clés de la Clean Architecture

Les dépendances doivent pointer vers l’intérieur : du monde extérieur (contrôleurs, API, base de données) vers le cœur métier (entités, cas d’utilisation). Les couches typiques incluent :

  • Entities : objets du domaine (règles invariantes).
  • Use Cases : application des règles métier (orchestration).
  • Interface Adapters : adaptateurs d’entrée/sortie (HTTP, DB, messaging).
  • Frameworks & Drivers : Spring, JPA, Angular, etc.

Découpage proposé pour Spring Boot

L’architecture peut être implémentée au niveau du projet Java via des packages et/ou des modules Maven/Gradle. L’objectif est de figer les frontières et d’empêcher des dépendances implicites.

Structure de packages recommandée

Exemple de structure :

src/
  main/
    java/
      com/
        company/
          app/
            domain/
              model/
                Customer.java
                Order.java
              services/
                PricingPolicy.java
            application/
              usecase/
                CreateOrderUseCase.java
                GetCustomerProfileUseCase.java
              port/
                output/
                  OrderRepository.java
                  CustomerQueryPort.java
                input/
                  CreateOrderCommand.java
                  OrderCreatedEvent.java
            adapters/
              inbound/
                http/
                  OrderController.java
                  dto/
                    CreateOrderRequest.java
              outbound/
                persistence/
                  JpaOrderRepository.java
                  mapper/
                    OrderEntityMapper.java
                messaging/
                  EventPublisher.java
            config/
              SpringBeans.java
  

Règles de dépendances (à faire respecter)

  • application dépend de domain.
  • adapters dépend de application.
  • adapters ne doit pas être invoqué depuis domain ou application.
  • frameworks (Spring/JPA) restent confinés dans adapters ou des modules dédiés.

Modéliser le domaine

Entités et invariants

Les entités du domaine portent les invariants. Elles n’utilisent pas d’annotations JPA et ne dépendent d’aucun mécanisme Spring. Les règles de validation sont implémentées à l’intérieur des objets.

package com.company.app.domain.model;

import java.math.BigDecimal;

public final class Order {
  private final String id;
  private final String customerId;
  private final BigDecimal total;

  public Order(String id, String customerId, BigDecimal total) {
    if (id == null || id.isBlank()) throw new IllegalArgumentException("id requis");
    if (customerId == null || customerId.isBlank()) throw new IllegalArgumentException("customerId requis");
    if (total == null || total.signum() < 0) throw new IllegalArgumentException("total invalide");
    this.id = id;
    this.customerId = customerId;
    this.total = total;
  }

  public String id() { return id; }
  public String customerId() { return customerId; }
  public BigDecimal total() { return total; }
}
    

Définir les use cases (cas d’utilisation)

Les use cases décrivent les actions applicatives. Ils orchestrent le domaine et s’appuient sur des ports. Les ports définissent des contrats abstraits pour la persistance, le messaging ou d’autres intégrations.

Use case d’exemple

package com.company.app.application.usecase;

import com.company.app.application.port.output.OrderRepository;
import com.company.app.domain.model.Order;

import java.math.BigDecimal;

public final class CreateOrderUseCase {

  private final OrderRepository orderRepository;

  public CreateOrderUseCase(OrderRepository orderRepository) {
    this.orderRepository = orderRepository;
  }

  public String execute(String customerId, BigDecimal total) {
    String orderId = java.util.UUID.randomUUID().toString();
    Order order = new Order(orderId, customerId, total);
    orderRepository.save(order);
    return orderId;
  }
}
    

Ports de sortie (contrats d’infrastructure)

package com.company.app.application.port.output;

import com.company.app.domain.model.Order;

public interface OrderRepository {
  void save(Order order);
}
    

Adapter l’entrée : contrôleurs HTTP

Les contrôleurs Spring agissent comme adaptateurs d’entrée. Ils traduisent la requête HTTP vers un modèle de commande, invoquent le use case, puis convertissent la réponse en format API.

Contrôleur Spring (adapter inbound)

package com.company.app.adapters.inbound.http;

import com.company.app.application.usecase.CreateOrderUseCase;
import com.company.app.adapters.inbound.http.dto.CreateOrderRequest;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

@RestController
@RequestMapping("/api/orders")
public class OrderController {

  private final CreateOrderUseCase createOrderUseCase;

  public OrderController(CreateOrderUseCase createOrderUseCase) {
    this.createOrderUseCase = createOrderUseCase;
  }

  @PostMapping
  public ResponseEntity<Map<String, String>> create(@RequestBody CreateOrderRequest request) {
    String orderId = createOrderUseCase.execute(
      request.customerId(),
      request.total()
    );
    return ResponseEntity.ok(Map.of("orderId", orderId));
  }
}
    

DTO d’entrée

package com.company.app.adapters.inbound.http.dto;

import java.math.BigDecimal;

public record CreateOrderRequest(String customerId, BigDecimal total) {}
    

Adapter la sortie : persistance JPA

Les implémentations des ports de sortie utilisent JPA ou toute autre technologie. Elles convertissent les entités de persistance en entités du domaine (mappage).

Implémentation d’un port

package com.company.app.adapters.outbound.persistence;

import com.company.app.application.port.output.OrderRepository;
import com.company.app.domain.model.Order;

public class JpaOrderRepository implements OrderRepository {

  private final OrderJpaCrudRepository crudRepository;
  private final OrderEntityMapper mapper;

  public JpaOrderRepository(OrderJpaCrudRepository crudRepository, OrderEntityMapper mapper) {
    this.crudRepository = crudRepository;
    this.mapper = mapper;
  }

  @Override
  public void save(Order order) {
    crudRepository.save(mapper.toEntity(order));
  }
}
    

Mapper (domaine → entité JPA)

package com.company.app.adapters.outbound.persistence.mapper;

import com.company.app.adapters.outbound.persistence.entity.OrderEntity;
import com.company.app.domain.model.Order;

public class OrderEntityMapper {
  public OrderEntity toEntity(Order order) {
    OrderEntity entity = new OrderEntity();
    entity.setId(order.id());
    entity.setCustomerId(order.customerId());
    entity.setTotal(order.total());
    return entity;
  }
}
    

Intégrer Angular avec une logique Clean Architecture

Côté front, une séparation similaire peut être appliquée. L’UI (components) ne contient pas directement les règles métier ; elle orchestre via des services applicatifs. Les appels HTTP sont encapsulés derrière des adaptateurs.

Structure Angular proposée

src/app/
  core/
    api/
      http-client.ts
      order-api.adapter.ts
  domain/
    model/
      order.ts
  application/
    usecases/
      create-order.usecase.ts
  presentation/
    pages/
      order-create.page.ts
    components/
      order-form.component.ts
  shared/
    dto/
      create-order-request.ts
      api-response.ts
    mappers/
      order.mapper.ts
    errors/
      app-errors.ts
    result.ts
  

Use case côté Angular

Le use case appelle un port côté front (adaptateur API) et implémente la coordination. Les règles métiers front peuvent rester très limitées si le domaine est central côté back.

// create-order.usecase.ts
import { OrderApiAdapter } from '../ports/order-api.adapter';
import { CreateOrderRequest } from '../../shared/dto/create-order-request';
import { Result } from '../../shared/result';

export class CreateOrderUseCase {
  constructor(private readonly orderApi: OrderApiAdapter) {}

  execute(request: CreateOrderRequest): Promise<Result<string>> {
    return this.orderApi.createOrder(request);
  }
}
    

Adaptateur HTTP (couche infrastructure)

// order-api.adapter.ts
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { CreateOrderRequest } from '../../shared/dto/create-order-request';
import { Result } from '../../shared/result';

@Injectable({ providedIn: 'root' })
export class OrderApiAdapter {
  constructor(private readonly http: HttpClient) {}

  async createOrder(request: CreateOrderRequest): Promise<Result<string>> {
    try {
      const res = await this.http.post<{ orderId: string }>('/api/orders', request).toPromise();
      return { ok: true, value: res!.orderId };
    } catch (e: any) {
      return { ok: false, error: e?.message ?? 'Erreur inconnue' };
    }
  }
}
    

Contrats API et mapping

La robustesse de l’ensemble dépend de la qualité des contrats. Les DTO API peuvent être définis comme des structures de communication, tandis que les modèles de domaine restent distincts. Des mappers permettent d’assurer la traduction entre API et modèle.

DTO de requête

// create-order-request.ts
export interface CreateOrderRequest {
  customerId: string;
  total: number;
}
    

Mapper (optionnel)

// order.mapper.ts
import { Order } from '../../domain/model/order';

export function toOrder(domain: any): Order {
  return {
    id: domain.id,
    customerId: domain.customerId,
    total: domain.total
  };
}
    

Gestion des dépendances et configuration Spring

La création des use cases peut être déléguée à la configuration Spring, en conservant l’abstraction. Les beans Spring sont placés dans une zone de composition, typiquement config.

Composition des dépendances

package com.company.app.config;

import com.company.app.adapters.outbound.persistence.JpaOrderRepository;
import com.company.app.adapters.outbound.persistence.OrderJpaCrudRepository;
import com.company.app.adapters.outbound.persistence.mapper.OrderEntityMapper;
import com.company.app.application.usecase.CreateOrderUseCase;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SpringBeans {

  @Bean
  public CreateOrderUseCase createOrderUseCase(
      OrderJpaCrudRepository crudRepository
  ) {
    var mapper = new OrderEntityMapper();
    var repo = new JpaOrderRepository(crudRepository, mapper);
    return new CreateOrderUseCase(repo);
  }
}
    

Stratégies de tests

Tests unitaires des use cases

Les use cases reçoivent des ports (interfaces) qui peuvent être mockés. Ainsi, le comportement métier est validé sans infrastructure.

// pseudo-test
// CreateOrderUseCaseTest
// - given mock OrderRepository
// - when execute()
// - then verify save() called with an Order valide
    

Tests d’adaptateurs

Les contrôleurs HTTP et les adaptateurs JPA peuvent être testés séparément avec des tests d’intégration, mais la logique métier doit rester testée principalement via les use cases.

Erreurs fréquentes à éviter

  • Annoter le domaine (par exemple JPA) : cela coule l’infrastructure dans le cœur.
  • Appeler Spring/JPA depuis les use cases : les use cases ne devraient utiliser que des ports.
  • Mélanger DTO et entités métier : les modèles doivent rester distincts.
  • Faire du front une copie du back : le back doit rester la référence des règles métier critiques.

Conclusion

La Clean Architecture appliquée à une application d’entreprise Spring Boot et Angular rend la base applicative plus robuste. En isolant les règles métier dans le domaine et les cas d’utilisation, puis en encapsulant l’UI et l’infrastructure dans des adaptateurs, le projet gagne en maintenabilité, en testabilité et en capacité d’évolution, tout en limitant les dépendances aux frameworks.

À 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

© 2026 Laty Gueye Samba.