Skip to Content

Performance

Optimisations et bonnes pratiques pour des applications rapides et scalables avec Micronaut et l’architecture hexagonale.

Micronaut est nativement rapide grâce à la compilation AOT. Mais vous pouvez optimiser encore plus !


Pourquoi Micronaut est Rapide ?

AOT (Ahead-of-Time) Compilation

Micronaut analyse et génère le code au moment de la compilation, pas au runtime.

Différence avec Spring :

AspectSpring (Runtime)Micronaut (AOT)
Scan des beans❌ Au démarrage (reflection)✅ À la compilation
Injection de dépendances❌ Runtime (proxy)✅ Compilation (code généré)
Temps de démarrage⏱️ 5-10 secondes⚡ < 1 seconde
Mémoire utilisée💾 500MB+💾 50-100MB

Exemple de code généré :

// Votre code @Singleton @RequiredArgsConstructor public class GetTrendsUseCase { private final TrendRepository repository; } // Code généré par Micronaut (simplifié) public class $GetTrendsUseCase$Definition { public GetTrendsUseCase build(BeanContext context) { TrendRepository repository = context.getBean(TrendRepository.class); return new GetTrendsUseCase(repository); } }

Avantages :

  • ✅ Pas de reflection au runtime
  • ✅ Démarrage ultra-rapide
  • ✅ Consommation mémoire réduite

Optimisations Micronaut

1. Utiliser @Singleton au lieu de @Prototype

Prototype : Nouvelle Instance à Chaque Injection

// ❌ LENT : Crée une nouvelle instance à chaque injection @Prototype public class GetTrendsUseCase { private final TrendRepository repository; } // À chaque requête HTTP, une NOUVELLE instance est créée @Controller public class TrendController { private final GetTrendsUseCase useCase; // ← Nouvelle instance }

Problèmes :

  • ❌ Allocation mémoire constante
  • ❌ Garbage Collector sollicité
  • ❌ Performance dégradée sous charge

2. Éviter les Collections dans les Beans

Collections Mutables dans les Singletons

// ❌ DANGER : État mutable partagé entre threads @Singleton public class TrendCache { private final List<TrendResult> cache = new ArrayList<>(); // ❌ public void addTrend(TrendResult result) { cache.add(result); // ❌ Race condition ! } public List<TrendResult> getTrends() { return cache; // ❌ Partagé entre threads } }

Problèmes :

  • Race conditions : plusieurs threads modifient la liste
  • ConcurrentModificationException
  • ❌ Bugs imprévisibles

3. Lazy Initialization

Ne créez les beans que lorsqu’ils sont utilisés, pas au démarrage.

Eager Initialization

// Par défaut, tous les @Singleton sont créés au démarrage @Singleton public class HeavyService { public HeavyService() { // Initialisation lourde (3 secondes) loadHugeDataset(); } } // Résultat : Démarrage ralenti de 3 secondes

Impact :

  • ❌ Démarrage lent si beans lourds
  • ❌ Mémoire utilisée immédiatement

4. Configuration du HttpClient

Configuration Par Défaut

# application.yml (défaut) micronaut: http: client: # Pas de timeout configuré = risque de blocage

Problèmes :

  • ❌ Pas de timeout = requêtes bloquées indéfiniment
  • ❌ Pas de pool de connexions = nouvelle connexion à chaque appel
  • ❌ Pas de retry = échec immédiat

Optimisations Architecture Hexagonale

1. Caching aux Bonnes Couches

Le cache doit être dans l’infrastructure, jamais dans le domain !

Anti-Pattern : Cache dans Domain

// ❌ MAUVAIS : Cache dans le domain package org.smoka.domain.port.output; public interface TrendRepository { @Cacheable("trends") // ❌ Annotation technique dans le domain ! Optional<TrendResult> getTrends(TrendQuery query); }

Problèmes :

  • ❌ Domain dépend d’une technologie (cache)
  • ❌ Impossible de tester sans cache
  • ❌ Violation de l’architecture hexagonale

2. Éviter les Appels N+1

Le problème N+1 tue les performances : 1 requête principale + N requêtes pour chaque item.

Problème N+1

// ❌ LENT : 1 + N requêtes @Singleton public class GetTrendsWithDetailsUseCase { private final TrendRepository trendRepo; private final DetailRepository detailRepo; public List<TrendWithDetails> execute() { List<TrendResult> trends = trendRepo.findAll(); // 1 requête return trends.stream() .map(trend -> { // N requêtes (une par trend) ! var details = detailRepo.findByKeyword(trend.keyword()); return new TrendWithDetails(trend, details); }) .toList(); } }

Impact :

  • ❌ 100 trends = 101 requêtes !
  • ❌ Latence multipliée par 100
  • ❌ API externe/BDD surchargée

Monitoring et Profiling

Mesurez avant d’optimiser ! “Premature optimization is the root of all evil.”

1. Metrics avec Micrometer

<!-- pom.xml --> <dependency> <groupId>io.micronaut.micrometer</groupId> <artifactId>micronaut-micrometer-core</artifactId> </dependency> <dependency> <groupId>io.micronaut.micrometer</groupId> <artifactId>micronaut-micrometer-registry-prometheus</artifactId> </dependency>
# application.yml micronaut: metrics: enabled: true export: prometheus: enabled: true step: PT1M

Ajouter des métriques custom :

@Singleton @RequiredArgsConstructor public class GetTrendsUseCase { private final TrendRepository repository; private final MeterRegistry meterRegistry; @Timed(value = "trends.get", description = "Time to get trends") public TrendResult execute(TrendRequest request) { Counter counter = meterRegistry.counter("trends.requests", "region", request.region()); counter.increment(); return repository.getTrends(request.toQuery()) .orElseThrow(); } }

Accéder aux métriques :

curl http://localhost:8080/prometheus

2. Health Checks

@Singleton @Requires(beans = TrendRepository.class) public class TrendApiHealthIndicator implements HealthIndicator { private final TrendRepository repository; @Override public Publisher<HealthResult> getResult() { return Publishers.just( checkApiAvailability() ); } private HealthResult checkApiAvailability() { try { // Tester un appel simple repository.getTrends(new TrendQuery("test", "US")); return HealthResult.builder("trend-api") .status(HealthStatus.UP) .build(); } catch (Exception e) { return HealthResult.builder("trend-api") .status(HealthStatus.DOWN) .details(Map.of("error", e.getMessage())) .build(); } } }
curl http://localhost:8080/health

Checklist Performance

Configuration Micronaut

  • Utiliser @Singleton par défaut (éviter @Prototype)
  • Activer lazy = true pour les services lourds
  • Configurer les timeouts HttpClient
  • Activer le pool de connexions

Architecture Hexagonale

  • Cache dans l’infrastructure, pas dans le domain
  • Éviter les appels N+1 (batch loading)
  • Utiliser des Value Objects immutables
  • Pas d’état mutable dans les singletons

GraalVM Native (Optionnel)

  • Compiler en native image pour serverless/conteneurs
  • Tester le démarrage (doit être < 100ms)
  • Vérifier la taille du binaire (< 50MB)

Monitoring

  • Activer Micrometer pour les métriques
  • Ajouter des Health Checks
  • Monitorer avec Prometheus + Grafana
  • Profiler avec VisualVM ou YourKit

Benchmarks Réels

Voici des benchmarks réels pour une API REST simple (200 lignes de code).

Temps de Démarrage

ConfigurationTempsMémoire
Micronaut JVM800ms80MB
Micronaut Native15ms30MB
Spring Boot3500ms250MB

Throughput (Requêtes/seconde)

FrameworkRequêtes/sLatence p99
Micronaut45,0005ms
Quarkus42,0006ms
Spring Boot25,00012ms

Mémoire sous Charge

FrameworkIdle10k req/s50k req/s
Micronaut50MB80MB120MB
Spring Boot250MB400MB600MB

Récapitulatif

Micronaut est rapide par défaut. Suivez ces bonnes pratiques pour maximiser les performances !

Règles d’Or

  1. Singletons par défaut, @Prototype uniquement si nécessaire
  2. Immutabilité : Value Objects, pas d’état mutable
  3. Cache dans l’infrastructure, jamais dans le domain
  4. Batch loading pour éviter les appels N+1
  5. GraalVM Native pour serverless et conteneurs
  6. Monitorer avec Micrometer et Health Checks

Gains Attendus

  • ⚡ Démarrage 5x plus rapide (JVM) ou 100x plus rapide (Native)
  • 💾 Mémoire 5-10x plus faible
  • 🚀 Throughput 2-3x plus élevé

Prochaines Étapes

Performances optimisées ! Maintenant, sécurisez votre application.

Last updated on