Boostez votre application Spring Boot grâce aux design patterns

Publié le 11/12/2025 Source : sfeir.dev

Depuis ses débuts, Spring Boot s’est imposé comme une référence dans le monde Java pour sa capacité à simplifier la création d’applications robustes, maintenables et prêtes à l’emploi. Cependant, derrière cette simplicité apparente se cache une architecture pensée avec rigueur, reposant sur des design patterns éprouvés — ces mêmes modèles de conception qui, depuis des décennies, guident les ingénieurs vers un code plus clair, plus souple et plus réutilisable.

Cet article a pour but de montrer comment les design patterns peuvent être utilisés consciemment par le développeur pour renforcer encore les qualités de Spring Boot, mais aussi de rappeler que le framework lui-même est bâti sur ces principes, souvent de manière invisible pour l’utilisateur.

Spring Boot : une mécanique interne fondée sur les design patterns

Spring Boot n’est pas seulement un outil facilitant la configuration des projets Java, c’est avant tout une application magistrale des design patterns.
Prenons quelques exemples emblématiques :

Ainsi, même sans en avoir conscience, tout développeur Spring Boot utilise quotidiennement ces patterns. Pourtant, les intégrer consciemment dans son propre code peut permettre de passer d’une application simplement fonctionnelle à une application élégante, évolutive et pérenne.

Pourquoi utiliser les design patterns dans vos projets Spring Boot ?

Les design patterns ne sont pas de simples artifices de style : ils offrent des solutions éprouvées à des problèmes récurrents du développement logiciel.
Appliqués dans un contexte Spring Boot, ils peuvent :

En somme, il s’agit d’un retour aux fondations du génie logiciel : concevoir avant de coder, et bâtir des systèmes pensés pour durer.

Quelques patterns particulièrement utiles dans une application Spring Boot

Avant de plonger dans un cas concret de CRUD enrichi, voici une sélection de patterns dont l’application dans un contexte Spring Boot est particulièrement féconde :

Vers un CRUD augmenté : l’application concrète des patterns

Pour illustrer concrètement la puissance des design patterns dans un contexte Spring Boot, construisons un petit module de gestion de livres.

Le modèle : le pattern Builder

Commençons par la classe Book.
Plutôt que d’utiliser des constructeurs surchargés ou des setters à répétition, nous faisons appel au pattern Builder.
Ce modèle permet de construire des objets complexes de manière lisible, fluide et immuable — un principe cher aux conceptions robustes.

public class Book {
    private final long id;
    private final String title;
    private final String author;
    private final String type;
    private final String cote;

    private Book(Builder builder) {
        this.id = builder.id;
        this.title = builder.title;
        this.author = builder.author;
        this.type = builder.type;
        this.cote = builder.cote;
    }

    public static class Builder {
        private long id;
        private String title;
        private String author;
        private String type;
        private String cote;

        public Builder id(long id) {
            this.id = id;
            return this;
        }

        public Builder title(String title) {
            this.title = title;
            return this;
        }

        public Builder author(String author) {
            this.author = author;
            return this;
        }

        public Builder type(String type) {
            this.type = type;
            return this;
        }

        public Builder cote(String cote) {
            this.cote = cote;
            return this;
        }

        public Book build() {
            return new Book(this);
        }
    }

    // Getters classiques
    public long getId() { return id; }
    public String getTitle() { return title; }
    public String getAuthor() { return author; }
    public String getType() { return type; }
    public String getCote() { return cote; }

    @Override
    public String toString() {
        return "Book{"
                + "id=" + id
                + ", title='" + title + '\''
                + ", author='" + author + '\''
                + ", type='" + type + '\''
                + ", cote='" + cote + '\''
                + '}';
    }
}

Notre modèle et son builder

Ici, le Builder permet une création claire :

Book book = new Book.Builder()
    .id(1L)
    .title("L'Étranger")
    .author("Camus")
    .type("Roman")
    .cote("R-CAMU")
    .build();

Ce pattern favorise la sécurité (objets immuables) et la lisibilité (chaînage fluide).

Le pattern Factory : déléguer la création

Spring repose déjà sur le principe de Factory, mais ici nous l’utilisons explicitement dans notre logique métier.
L’idée est de centraliser la création des livres dans une classe dédiée (BookFactory), capable d’intégrer d’autres patterns (comme la Strategy).

@Component
public class BookFactory {

    private final BookCoteStrategyFactory coteStrategyFactory;

    public BookFactory(BookCoteStrategyFactory coteStrategyFactory) {
        this.coteStrategyFactory = coteStrategyFactory;
    }

    public Book createBook(long id, String title, String author, String type) {
        BookCoteStrategy strategy = coteStrategyFactory.getStrategy(type);
        String cote = strategy.generateCote(author);

        return new Book.Builder()
                .id(id)
                .title(title)
                .author(author)
                .type(type)
                .cote(cote)
                .build();
    }
}

la factory

Ainsi, la création d’un livre ne dépend plus du code client.
Si demain la logique de génération de cote évolue, il suffira de modifier la factory, pas les services.

Le pattern Strategy : un comportement interchangeable

Nous devons maintenant gérer la génération de la cote (code interne) du livre selon son type.
Le pattern Strategy permet d’externaliser ce comportement dans des classes dédiées, facilement interchangeables.

L’interface de stratégie

public interface BookCoteStrategy {
    String generateCote(String author);

    // Identifiant de la stratégie (clé utilisée pour la Map)
    String getType();
}

l’interface strategie

Implémentations concrètes

@Component
public class BdCoteStrategy implements BookCoteStrategy {
    public String generateCote(String author) {
        return "BD-" + author.substring(0, Math.min(4, author.length())).toUpperCase();
    }
    public String getType() { return "BD"; }
}

@Component
public class RomanCoteStrategy implements BookCoteStrategy {
    public String generateCote(String author) {
        return "R-" + author.substring(0, Math.min(4, author.length())).toUpperCase();
    }
    public String getType() { return "Roman"; }
}

@Component
public class DefaultCoteStrategy implements BookCoteStrategy {
    public String generateCote(String author) {
        return author.substring(0, Math.min(4, author.length())).toUpperCase();
    }
    public String getType() { return "Default"; }
}

Les différentes implémentations de notre stratégie

La Factory des stratégies

@Component
public class BookCoteStrategyFactory {

    private final Map<String, BookCoteStrategy> strategies;

    public BookCoteStrategyFactory(Map<String, BookCoteStrategy> strategies) {
        this.strategies = strategies;
    }

    public BookCoteStrategy getStrategy(String type) {
        return strategies.values().stream()
                .filter(s -> s.getType().equalsIgnoreCase(type))
                .findFirst()
                .orElseGet(() -> strategies.values().stream()
                        .filter(s -> "Default".equalsIgnoreCase(s.getType()))
                        .findFirst()
                        .orElseThrow(() -> new IllegalStateException("Default strategy not found")));
    }
}

Grâce à Spring, les stratégies sont injectées automatiquement.

En effet, lorsqu’une classe est annotée avec @Component, elle devient un bean Spring :

Ainsi, lorsque Spring rencontre le constructeur suivant :

public BookCoteStrategyFactory(Map<String, BookCoteStrategy> strategies)

l sait qu’il doit :

  1. Chercher tous les beans du type BookCoteStrategy dans le contexte,
  2. Créer une Map où :
    • la clé est le nom du bean (par défaut, le nom de la classe avec la première lettre en minuscule, par exemple bdCoteStrategy),
    • la valeur est l’instance du bean (BdCoteStrategyRomanCoteStrategyDefaultCoteStrategy),
  3. Injecter cette Map dans le constructeur.

Concrètement Spring fait pour nous la chose suivante :

strategies = Map.of(
    "bdCoteStrategy", new BdCoteStrategy(),
    "romanCoteStrategy", new RomanCoteStrategy(),
    "defaultCoteStrategy", new DefaultCoteStrategy()
);

Il n’y a donc aucune configuration manuelle ni injection multiple à écrire :
le framework applique automatiquement le principe du pattern Factory, couplé à l’inversion de contrôle (IoC).

Le Service et le Controller : un CRUD clair et structuré

Le service

@Service
public class BookService {

    private final BookFactory bookFactory;
    private final List<Book> books = new ArrayList<>();
    private final AtomicLong counter = new AtomicLong();

    public BookService(BookFactory bookFactory) {
        this.bookFactory = bookFactory;
    }

    public Book createBook(String title, String author, String type) {
        long id = counter.incrementAndGet();
        Book book = bookFactory.createBook(id, title, author, type);
        books.add(book);
        return book;
    }

    public List<Book> getAllBooks() { return books; }

    public Optional<Book> getBookById(long id) {
        return books.stream().filter(b -> b.getId() == id).findFirst();
    }

    public Optional<Book> updateBook(long id, String title, String author, String type) {
        for (int i = 0; i < books.size(); i++) {
            if (books.get(i).getId() == id) {
                Book updated = bookFactory.createBook(id, title, author, type);
                books.set(i, updated);
                return Optional.of(updated);
            }
        }
        return Optional.empty();
    }

    public boolean deleteBook(long id) {
        return books.removeIf(b -> b.getId() == id);
    }
}

le service

Le controller REST

@RestController
@RequestMapping("/books")
public class BookController {

    private final BookService service;

    public BookController(BookService service) {
        this.service = service;
    }

    @PostMapping
    public Book createBook(@RequestParam String title,
                           @RequestParam String author,
                           @RequestParam String type) {
        return service.createBook(title, author, type);
    }

    @GetMapping
    public List<Book> listBooks() {
        return service.getAllBooks();
    }

    @GetMapping("/{id}")
    public ResponseEntity<Book> getBookById(@PathVariable long id) {
        return service.getBookById(id)
                .map(ResponseEntity::ok)
                .orElse(ResponseEntity.notFound().build());
    }

    @PutMapping("/{id}")
    public ResponseEntity<Book> updateBook(@PathVariable long id,
                                           @RequestParam String title,
                                           @RequestParam String author,
                                           @RequestParam String type) {
        return service.updateBook(id, title, author, type)
                .map(ResponseEntity::ok)
                .orElse(ResponseEntity.notFound().build());
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteBook(@PathVariable long id) {
        return service.deleteBook(id)
                ? ResponseEntity.noContent().build()
                : ResponseEntity.notFound().build();
    }
}

le controller

L’ensemble offre une architecture claire, où chaque responsabilité est bien séparée :

Le pattern Proxy : l’AOP pour un logging transversal

Enfin, ajoutons une couche de logging via le pattern Proxy, ici implémenté à travers Spring AOP.
Le principe : intercepter les appels aux méthodes de service pour tracer leur exécution sans toucher au code métier.

@Aspect
@Component
public class LoggingAspect {

    private static final Logger log = LoggerFactory.getLogger(LoggingAspect.class);

    @Pointcut("within(fr.eletutour.designpattern.service..*)")
    public void serviceMethods() {}

    @Around("serviceMethods()")
    public Object logExecution(ProceedingJoinPoint joinPoint) throws Throwable {
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();

        log.info("==> Entering method: {} with arguments: {}", methodName, Arrays.toString(args));

        long startTime = System.currentTimeMillis();
        Object result = joinPoint.proceed();
        long endTime = System.currentTimeMillis();

        log.info("<== Exiting method: {} with result: {}. Execution time: {} ms",
                 methodName, result, endTime - startTime);

        return result;
    }
}

Ce proxy agit en décorateur du service, ajoutant une fonctionnalité transversale (le logging) sans altérer le code métier.
Une illustration parfaite du principe open/closed : ouvert à l’extension, fermé à la modification.

Conclusion

L’exemple de ce CRUD de bibliothèque illustre parfaitement comment les design patterns, loin d’être de simples exercices théoriques, demeurent des piliers intemporels du développement logiciel.
Spring, par son architecture même, en est une incarnation moderne : il ne fait pas que supporter les patterns — il les institutionnalise.

Ce mariage entre la tradition des design patterns et la philosophie de Spring repose sur une idée simple :

« Le code doit être ouvert à l’extension, mais fermé à la modification. » — Bertrand Meyer

Chaque pattern que nous avons implémenté honore ce principe.
Le développeur n’a plus à lier lui-même les composants : il se contente de décrire l’intention. Le reste est pris en charge par le conteneur Spring, héritier du principe d’inversion de contrôle.

Ainsi, au lieu de “programmer contre des classes”, nous programmons contre des contrats, et la flexibilité devient naturelle.
Ce n’est pas un hasard si ces patterns, nés dans les années 90, trouvent une seconde jeunesse dans l’écosystème Spring : ils partagent une même philosophie — structurer la complexité sans renier la clarté.

En définitive, combiner BuilderFactoryStrategy et Aspect au sein d’une application Spring Boot, c’est renouer avec l’esprit fondateur du développement orienté objet :
un code clair, modulaire, extensible, et fidèle à la tradition logicielle — celle où chaque ligne a du sens.