Luca Mele
Luca Mele

Architecture

Supprimez le BFF Java : pourquoi les petites équipes devraient passer au fullstack avec Next.js

Supprimez le BFF Java : pourquoi les petites équipes devraient passer au fullstack avec Next.js
Retour aux articles
·11 min de lecture
Écouter le podcast généré par IA
Tous les podcasts →

Voici un setup que j'ai vu dans plusieurs entreprises : une petite équipe Scrum — peut-être 6 à 8 personnes — qui maintient un frontend React et un « Backend for Frontend » (BFF) en Java Spring Boot. Le BFF ne possède pas de logique métier. Il récupère des données depuis des microservices, les transforme et les sert en JSON à l'app React. C'est un proxy avec des types.

Cette architecture avait du sens quand elle a été introduite. L'équipe backend écrivait en Java, l'équipe frontend en JavaScript, et le BFF était leur point de rencontre. Mais en 2026, avec TypeScript mature, Next.js éprouvé au combat, et Node.js qui surpasse la JVM pour les workloads I/O, la question mérite d'être posée : devrait-on encore maintenir deux écosystèmes pour ce qui est essentiellement un seul produit ?

Je vais plaider pour la consolidation vers Next.js — mais je serai aussi honnête sur les points où Java gagne. Ce n'est pas un avis à chaud. C'est une décision d'architecture qui devrait être prise avec des chiffres, pas des sentiments.

Le pattern BFF : ce que c'est et pourquoi il existe

Le pattern Backend for Frontend, inventé par Sam Newman, existe pour une bonne raison : différents clients (web, mobile, TV) ont besoin de formes de données différentes depuis les mêmes services backend. Plutôt que de forcer le frontend à orchestrer plusieurs appels API et transformer les données, le BFF le fait côté serveur.

En pratique, la plupart des BFF que j'ai vus dans les petites équipes font trois choses : agréger plusieurs appels de service en une réponse, transformer les données dans la forme dont l'UI a besoin, et gérer l'authentification/gestion de session. C'est tout. Pas de logique métier complexe. Pas de calcul lourd. Juste de l'orchestration I/O.

La question n'est pas de savoir si vous avez besoin d'une couche BFF — probablement oui. La question est de savoir si cette couche doit être une application Java séparée maintenue par des personnes séparées avec un pipeline de build séparé.

Le même endpoint, deux mondes

Regardons le même endpoint BFF dans les deux stacks. Voici la version Java — du code Spring Boot propre et bien structuré :

// Java Spring Boot BFF — ProductController.java
@RestController
@RequestMapping("/api/products")
public class ProductController {

    @GetMapping("/{id}")
    public ProductResponse getProduct(@PathVariable Long id) {
        Product product = productService.findById(id);
        return new ProductResponse(
            product.getId(),
            product.getName(),
            product.getPrice().toPlainString(),
            product.isAvailable()
        );
    }
}

// ProductResponse.java — a separate DTO class
public record ProductResponse(
    Long id,
    String name,
    String price,
    boolean available
) {}

Et voici l'équivalent Next.js :

// Next.js — same logic, same language as the frontend
// app/api/products/[id]/route.ts
import { getProduct } from '@/lib/products';
import type { Product } from '@/types/product'; // shared with UI

export async function GET(
  _req: Request,
  { params }: { params: { id: string } }
) {
  const product: Product = await getProduct(params.id);
  return Response.json(product);
}

// The Product type is the same one the React component uses.
// No code generation. No schema drift. No mapping layer.

Le code est de longueur similaire. La différence n'est pas dans l'endpoint — elle est dans tout ce qui l'entoure. La version Java nécessite un projet séparé, des dépendances séparées (Spring Boot, Jackson, Gradle/Maven), un CI/CD séparé, une image Docker séparée, un déploiement séparé. La version Next.js est un fichier dans le même projet où vivent vos composants React.

Argument 1 : le partage de schéma élimine une classe entière de bugs

C'est l'argument le plus fort pour l'unification, et de loin. Quand votre BFF est en Java et votre frontend en TypeScript, vous avez deux systèmes de types qui doivent rester synchronisés. La solution typique est la génération de code OpenAPI/Swagger : le côté Java génère une spec, le frontend génère des types TypeScript à partir de celle-ci.

Ça fonctionne — jusqu'à ce que ça ne fonctionne plus. La spec se périme. Quelqu'un modifie le DTO Java mais oublie de régénérer. L'étape CI qui génère les types s'exécute après le merge du PR. Un champ est renommé en Java mais l'ancien nom persiste dans les types générés jusqu'au prochain rebuild complet. J'ai vu des bugs de production pour chacun de ces scénarios.

Avec un stack TypeScript unifié, ce problème entier disparaît. Pas « s'améliore » — disparaît. Vous définissez le type une fois, et la route API comme le composant React importent le même fichier. Renommez un champ, et le compilateur attrape chaque utilisation instantanément. Partagez une fonction de validation entre serveur et client. Pas d'étape de génération, pas de synchronisation, pas de dérive.

// types/product.ts — ONE type, used everywhere
export interface Product {
  id: string;
  name: string;
  price: number;
  available: boolean;
  variants: Variant[];
}

// Server: used in API route, validation, DB query typing
// Client: used in React component props, form state, tests
// No OpenAPI generation, no code-gen CI step, no version mismatch

// Shared validation — also runs on both sides
export function validateProduct(p: Partial<Product>): string[] {
  const errors: string[] = [];
  if (!p.name?.trim()) errors.push('Name is required');
  if (p.price != null && p.price < 0) errors.push('Price must be positive');
  return errors;
}

Argument 2 : efficacité d'équipe et bus factor

Dans un split traditionnel, vous avez besoin de spécialistes des deux côtés. Les développeurs Java ne peuvent pas contribuer de manière significative au code React (et ne le veulent généralement pas). Les développeurs frontend ne peuvent pas toucher au BFF (et n'en ont souvent pas le droit). Cela crée des goulots d'étranglement : un dev backend est malade, et personne ne peut modifier l'API. Un dev frontend est en vacances, et un fix UI critique attend.

Avec un stack unifié, chaque développeur peut travailler sur chaque couche. Une tâche « frontend » qui nécessite un changement d'API ? Un développeur, un PR, une review. Pas de coordination inter-équipes, pas de négociation de contrat API, pas d'attente que l'autre côté déploie en premier.

Le bus factor — le nombre de personnes qui peuvent être percutées par un bus avant que le projet ne s'arrête — s'améliore dramatiquement. Au lieu de 1-2 personnes qui comprennent chaque côté, vous avez 3-4 personnes qui comprennent l'ensemble.

Et il y a l'argument du headcount. Si votre BFF est une fine couche I/O (ce que la plupart sont), vous n'avez pas besoin de développeurs backend dédiés. Trois développeurs TypeScript solides peuvent remplacer deux backend + deux frontend, avec un meilleur débit car ils ne sont pas bloqués par les handoffs.

Traditional BFF setup (8 people):
┌─────────────────────────────────┐
│  2 Backend Devs (Java/Spring)   │ ← Own ecosystem, own tooling
│  1 Backend Lead                 │
├─────────────────────────────────┤
│  2 Frontend Devs (React/TS)     │ ← Different ecosystem
│  1 Frontend Lead                │
├─────────────────────────────────┤
│  1 QA · 1 Scrum Master          │
└─────────────────────────────────┘
  API contract sync: OpenAPI / Swagger
  Schema drift: constant risk
  Bus factor: 1 per side

Unified fullstack setup (6 people):
┌─────────────────────────────────┐
│  3 Fullstack Devs (TS/Next.js)  │ ← One ecosystem
│  1 Tech Lead                    │
├─────────────────────────────────┤
│  1 QA · 1 Scrum Master          │
└─────────────────────────────────┘
  Schema: shared TypeScript types
  Bus factor: any dev can work on any layer
  25% less headcount, same throughput

Argument 3 : un écosystème au lieu de deux

Deux stacks technologiques signifient deux systèmes de gestion de dépendances (npm + Maven/Gradle), deux pipelines CI, deux images Docker de base, deux ensembles de patches de sécurité à suivre, deux linters, deux formatters, deux frameworks de test, deux configurations de débogueur. Ce n'est pas juste le runtime — c'est tout l'overhead opérationnel.

Un seul écosystème signifie un package.json, une config CI, un Dockerfile, un ensemble de dépendances à auditer, une config linter, une façon d'écrire les tests. Les nouveaux développeurs s'intègrent plus vite car il y a une chose à apprendre, pas deux.

Ça semble petit, mais ça se cumule. Chaque CVE dans une dépendance Spring Boot est un ticket. Chaque upgrade majeur de Java est un projet. Chaque conflit de plugin Gradle est un après-midi. Avec un seul écosystème, vous divisez cette taxe opérationnelle par deux.

Argument 4 : performance cloud et coûts

Pour les workloads BFF liés aux I/O — ce que la plupart des BFF sont — Node.js utilise significativement moins de mémoire et démarre dramatiquement plus vite que la JVM. C'est important pour le serverless (cold starts) et les déploiements conteneurisés (densité et vitesse de scale-up).

L'overhead mémoire de la JVM vient de son runtime : chargement de classes, warmup de compilation JIT, métadonnées du garbage collector. Une app Spring Boot qui ne fait rien utilise 200-400 Mo. Un serveur Node.js qui ne fait rien utilise 30-50 Mo. Pour un BFF qui fetch, transforme et sert du JSON, cet overhead est du pur gaspillage.

L'event loop de Node.js est aussi architecturalement adapté aux workloads BFF. Un BFF passe la plupart de son temps à attendre les réponses des services amont — exactement le type de travail intensif en I/O et léger en CPU où le modèle async single-threaded de Node.js surpasse les modèles thread-per-request.

Cloud cost comparison (BFF serving JSON, ~1000 req/s):

Java Spring Boot (JVM):
  Memory:     512 MB–1 GB baseline (JVM heap)
  Cold start: 3–8 seconds
  Container:  ~350 MB image (JDK + fat JAR)
  Monthly:    ~$180–280 (2 vCPU, 1 GB RAM)

Node.js (Next.js API routes):
  Memory:     80–150 MB baseline
  Cold start: 200–800 ms
  Container:  ~80 MB image (Alpine + Node)
  Monthly:    ~$60–120 (1 vCPU, 512 MB RAM)

For I/O-bound BFF workloads (fetch → transform → respond),
Node.js uses 50–70% less memory and starts 5–10x faster.
Java wins at CPU-heavy computation, not at proxying JSON.

Les contre-arguments honnêtes : où Java gagne

Si c'était une décision simple, tout le monde l'aurait déjà prise. Java a des avantages réels et significatifs que vous devriez peser honnêtement :

Sûreté de typage et correction : le système de types de Java est plus strict et plus mature que celui de TypeScript. TypeScript a des échappatoires « any », un typage structurel qui peut cacher des divergences, et pas de vérification de types au runtime. Les types nominaux de Java, les exceptions vérifiées et les garanties au compile-time attrapent des catégories entières de bugs que TypeScript laisse passer. Pour les systèmes critiques (finance, santé), c'est important.

Travail intensif en CPU : si votre BFF fait des calculs significatifs — agrégation de données, chiffrement, transformations complexes — le compilateur JIT et le modèle de threading optimisé de la JVM surpasseront significativement Node.js. Node.js est single-threaded par design. Vous pouvez contourner avec des worker threads, mais vous luttez contre l'architecture.

Écosystème mature pour l'entreprise : l'écosystème de Spring Boot pour la sécurité (Spring Security), l'observabilité (Micrometer/Actuator) et l'intégration entreprise (Spring Cloud) est profond et éprouvé. Les équivalents Node.js existent mais sont moins matures et moins standardisés.

Talent et recrutement : dans beaucoup d'entreprises, les développeurs Java sont plus faciles à trouver que des développeurs fullstack TypeScript solides. Si le vivier de talents de votre entreprise est orienté Java, forcer une migration crée des frictions de recrutement.

Investissement existant : si vous avez des années de code BFF Java bien testé, bien structuré avec des tests d'intégration complets, un monitoring solide et un savoir institutionnel — réécrire a un coût réel et un risque réel. La nouvelle version aura de nouveaux bugs. « Ne jamais réécrire » est trop fort, mais « ne pas réécrire sans raison impérieuse » est de l'ingénierie solide.

Quand migrer, quand rester

Migrez quand : votre BFF est une fine couche I/O avec un minimum de logique métier. Votre équipe est petite (moins de 10) et les handoffs entre frontend/backend sont un goulot d'étranglement. Vous utilisez déjà TypeScript côté frontend. La dérive de schéma cause régulièrement des bugs. Vous passez un temps significatif à maintenir deux pipelines CI/CD. Les coûts cloud sont une préoccupation et vous avez des workloads I/O-bound.

Restez avec Java quand : votre BFF a une logique métier significative ou des calculs. Vous êtes dans une industrie réglementée où la sûreté de typage et l'outillage de Java (analyse statique, vérification formelle) apportent une valeur de conformité. Votre équipe a une expertise Java profonde et une expérience TypeScript limitée. Le BFF est stable, change rarement et ne cause pas de friction développeur. Vous faites partie d'un écosystème Java plus large où le service mesh Spring Cloud, les librairies partagées et l'outillage entreprise apportent une valeur significative.

La pire option est une semi-migration : déplacer certains endpoints vers Next.js tout en gardant les autres en Java. Cela vous donne trois écosystèmes à maintenir au lieu de deux. Si vous migrez, engagez-vous pleinement. Si Java est le bon choix, assumez cette décision et investissez pour rendre le BFF Java excellent.

Comment migrer de manière incrémentale

Si vous décidez de consolider, ne faites pas une réécriture big bang. Utilisez le pattern strangler fig : placez Next.js devant comme reverse proxy, migrez un endpoint à la fois, et arrêtez le service Java quand le dernier endpoint a été déplacé.

Commencez par les endpoints les plus simples et les plus modifiés — ceux qui causent le plus de friction développeur. Migrez d'abord les types (ils sont maintenant partagés), puis les routes, puis les tests. Chaque migration est un petit PR reviewable.

Faites tourner les deux systèmes en parallèle avec comparaison de trafic (shadow mode) pour détecter les différences comportementales. Le BFF Java sert le trafic de production pendant que la version Next.js traite les mêmes requêtes en arrière-plan. Comparez les réponses. Quand elles correspondent, basculez.

Cette approche vous permet de prouver le concept à faible risque, de bâtir la confiance de l'équipe et de vous arrêter à tout moment si la migration n'apporte pas de valeur. Les décisions d'architecture devraient être réversibles quand c'est possible — celle-ci l'est.

La conclusion

Pour une petite équipe Scrum qui maintient un BFF Java fin à côté d'un frontend React, l'approche Next.js unifiée offre : 25% de headcount en moins pour le même débit, zéro dérive de schéma entre API et UI, un bus factor plus élevé (tout dev peut travailler sur toute couche), 50-70% de coûts cloud en moins pour les workloads I/O, et la moitié de l'overhead opérationnel.

Mais ce n'est pas une réponse universelle. Les forces de Java en sûreté de typage, performance CPU et outillage entreprise sont réelles. La décision dépend de votre équipe, votre workload et vos contraintes.

Ce que je conteste, c'est le défaut — l'hypothèse que « backend = Java, frontend = React » est la seule façon de structurer une équipe. Pour beaucoup de petites équipes, ce n'est pas la façon optimale. La meilleure architecture est celle qui permet à votre équipe de livrer avec le moins de friction. Parfois ce sont deux écosystèmes avec des frontières claires. De plus en plus, pour les workloads BFF, c'en est un seul.