Appliquer le Domain‑Driven Design (DDD) en Rust pour une logique métier complexe
Par Emmanuel Forgues - 17 septembre 2025
Chapô – La compétitivité reposant désormais sur des processus métiers sophistiqués, les équipes techniques recherchent des langages alliant sécurité, performance et expressivité. Le Domain‑Driven Design (DDD), méthode éprouvée pour structurer la logique métier, rencontre le langage Rust, reconnu pour son modèle de possession et ses garanties d’absence de data races. Cet article analyse l'articulation entre DDD et Rust pour concevoir des systèmes maintenables et évolutifs, en détaillant les principes clés, les choix architecturaux, un exemple concret d’implémentation et les limites à considérer avant d'engager une organisation dans cette voie.
Publié initialement le 17 septembre 2025.
Mis à jour le 26 avril 2026.
Migré vers StratoSentry le 18 mai 2026.
Introduction – Un besoin pressant de solidité métier
Imaginez une fintech gérant des flux de paiement en temps réel sous contrainte de conformité réglementaire (ex.<0xE2><0x80><0xAF>: lutte contre le blanchiment). La logique métier — calculs de frais, validation de seuils, agrégation de comptes — est à la fois complexe et critique. Une erreur peut entraîner des pertes financières ou des sanctions légales.
Les approches basées sur des frameworks dynamiques (Ruby on Rails, Node.js) permettent un prototypage rapide mais peinent à assurer la fiabilité nécessaire à grande échelle. À l'inverse, les langages bas‑niveau comme C++ offrent le contrôle requis mais imposent une charge cognitive élevée et un risque d’erreurs de mémoire.
Rust, via son système d’emprunt (ownership) et son typage fort, assure la sécurité mémoire sans garbage collector et des performances proches du natif. Couplé à la méthodologie DDD — qui place le domaine métier au cœur de l’architecture — il permet de formaliser les règles métiers dans le code tout en bénéficiant des garanties de compilation.
Cet article s’adresse aux directeurs techniques, aux architectes logiciels et aux développeurs senior souhaitant évaluer la pertinence de cette combinaison pour des projets à forte valeur métier.
1️⃣ Contexte et enjeux du développement orienté domaine
| Enjeu | Impact sur le projet |
|---|---|
| Complexité fonctionnelle | Multiplicité de règles, contraintes légales, évolutions fréquentes. |
| Exigences de fiabilité | Nécessité d’éviter les bugs qui compromettent la conformité ou la sécurité financière. |
| Scalabilité | Le système doit supporter un trafic croissant sans perte de latence. |
| Coût de maintenance | Réduire la dette technique et faciliter l’ajout de nouvelles fonctionnalités. |
Le DDD répond à ces exigences via :
- Ubiquitous Language – vocabulaire partagé entre experts métier et développeurs.
- Bounded Contexts – découpage du domaine en sous‑domaines autonomes, limitant le couplage.
- Aggregates – regroupement d’entités autour d’une racine garantissant la cohérence transactionnelle.
L'implémentation de ces concepts dans un langage empêchant les erreurs classiques (null pointer, data race) fournit une base solide pour la logique métier.
2️⃣ Principes fondamentaux du Domain‑Driven Design
- Modélisation du domaine – Créer des Entités (identité persistante) et des Value Objects (immuables, définis par leurs attributs).
- Aggregates & Racine d’Agrégat – Définir une frontière de cohérence ; toutes les modifications passent par la racine.
- Repositories – Abstractions permettant de persister/retrouver des agrégats sans exposer l’infrastructure.
- Domain Services – Opérations qui ne trouvent pas naturellement leur place dans une entité ou un value object.
- Application Layer – Orchestration des use‑cases, coordination entre le domaine et les services d’infrastructure.
- Infrastructure – Implémentations concrètes (bases de données, bus de messages) injectées via l’inversion de contrôle.
Ces couches sont souvent représentées par une architecture hexagonale ou Ports & Adapters, qui s’accorde naturellement avec le système de modules de Rust.
3️⃣ Pourquoi choisir Rust pour la logique métier complexe
| Attribut | Rust | Langages concurrents |
|---|---|---|
| Sécurité mémoire | Ownership, Borrow Checker → aucune fuite, aucun use‑after‑free. | GC (Java, C#) ou vérifications à l’exécution (Python). |
| Absence de data races | Concurrence sûre grâce au typage (Send/Sync). | Verrouillages manuels (C++), risques élevés. |
| Performances | Compilation en code natif, optimisation LLVM. | Interprétés ou JIT, overhead supplémentaire. |
| Typage fort & génériques | Types immuables par défaut, traits pour abstractions. | Typage dynamique ou moins expressif (Go). |
| Écosystème async moderne | async/await, runtime Tokio, actix‑web. | Support async variable selon les langages. |
Ces caractéristiques permettent de codifier les invariants métier directement dans le type system, réduisant ainsi la surface d’erreur à l’exécution. Par exemple, un Money value object peut être défini avec une précision fixe et une devise validée au moment de la compilation grâce aux traits.
4️⃣ Architecture hexagonale adaptée à DDD en Rust
+---------------------------+
| Interface Utilisateur | ← Ports (Web, CLI)
+------------^--------------+
|
+------v------+ Application Service
| Use‑case | ←─────> (application layer)
+------+------+ |
| |
+--------v---------+ |
| Domaine (core) |←──────┘
| – Entités |
| – Value Objects |
| – Domain Services|
+--------^---------+
|
+--------v----------+ Infrastructure
| Repositories Impl.| ← DB, Message Bus, etc.
+-------------------+
- Ports (ex. : API REST avec actix‑web) exposent les use‑cases.
- Adapters implémentent les repositories (ex. : diesel pour PostgreSQL) et sont injectés via des traits (dyn OrderRepository).
- Le cœur du domaine ne dépend d’aucune bibliothèque externe, ce qui facilite les tests unitaires et la réutilisation.
Rust offre deux avantages majeurs à cette architecture<0xE2><0x80><0xAF>:
- Le système de modules (pub(crate), pub) permet de restreindre l’accès aux entités uniquement via leurs agrégats, renforçant l’encapsulation décrite par DDD.
- Les traits génériques servent de contrats d’interface (ports), tout en étant résolus à la compilation, évitant le coût du polymorphisme dynamique lorsqu’il n’est pas nécessaire.
5️⃣ Modélisation du domaine : entités, value objects et agrégats
5.1 Exemple de Value Object – Money
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Money {
amount: Decimal,
currency: Currency,
}
impl Money {
pub fn new(amount: Decimal, currency: Currency) -> Self {
// Validation simple : pas de montant négatif
assert!(amount >= Decimal::ZERO, "Le montant ne peut être négatif");
Money { amount, currency }
}
pub fn add(&self, other: &Money) -> Result<Money, &'static str> {
if self.currency != other.currency {
return Err("Devise différente");
}
Ok(Money::new(self.amount + other.amount, self.currency.clone()))
}
}
Immuable, le type Money garantit à la compilation que les opérations de calcul ne peuvent être appliquées qu’à des devises compatibles.
5.2 Entité – Order
use uuid::Uuid;
#[derive(Debug)]
pub struct Order {
id: Uuid,
items: Vec<OrderItem>,
total: Money,
status: OrderStatus,
}
L’identité (id) persiste même si les attributs changent, conformément au concept d’entité DDD.
5.3 Agrégat – OrderRoot (racine)
pub trait OrderRepository {
fn save(&self, order: &Order) -> Result<(), RepositoryError>;
fn find_by_id(&self, id: Uuid) -> Result<Option<Order>, RepositoryError>;
}
impl Order {
pub fn add_item(&mut self, item: OrderItem) -> Result<(), &'static str> {
// Invariant : le total ne doit jamais dépasser un plafond métier
let new_total = self.total.add(&item.price)?;
if new_total.amount > Decimal::from(10_000) {
return Err("Plafond de commande dépassé");
}
self.items.push(item);
self.total = new_total;
Ok(())
}
pub fn confirm(&mut self) -> Result<(), &'static str> {
if self.items.is_empty() {
return Err("Impossible de confirmer une commande vide");
}
self.status = OrderStatus::Confirmed;
Ok(())
}
}
Toutes les mutations passent par la racine d’agrégat (Order), garantissant le respect des invariants (plafond, non‑vide).
6️⃣ Implémentation des repositories et de l’infrastructure
6.1 Trait du repository (port)
pub trait OrderRepository: Send + Sync {
fn insert(&self, order: &Order) -> Result<(), RepositoryError>;
fn update(&self, order: &Order) -> Result<(), RepositoryError>;
fn get(&self, id: Uuid) -> Result<Option<Order>, RepositoryError>;
}
Le trait est object‑safe, permettant l’usage du dynamic dispatch au niveau de l’injection de dépendances.
6.2 Implémentation avec Diesel (adapter)
use diesel::prelude::*;
use crate::domain::{Order, OrderRepository};
pub struct DieselOrderRepo {
pool: sqlx::PgPool,
}
#[async_trait::async_trait]
impl OrderRepository for DieselOrderRepo {
async fn insert(&self, order: &Order) -> Result<(), RepositoryError> {
// Mapping manuel ou via `diesel_derive_newtype`…
unimplemented!()
}
async fn update(&self, order: &Order) -> Result<(), RepositoryError> {
unimplemented!()
}
async fn get(&self, id: Uuid) -> Result<Option<Order>, RepositoryError> {
unimplemented!()
}
}
Remarque<0xE2><0x80><0xAF>: async_trait permet d’exposer des méthodes asynchrones tout en conservant la signature du trait.
6.3 Injection de dépendances avec tower::ServiceBuilder
use tower::ServiceBuilder;
let repo = DieselOrderRepo { pool };
let app_state = AppState {
order_repo: Arc::new(repo),
};
let app = ServiceBuilder::new()
.layer(Extension(app_state))
.service(router);
Cette approche sépare le domaine (indépendant de diesel) de l’infrastructure, selon les principes DDD.
7️⃣ Gestion des transactions et cohérence éventuelle
L'atomicité globale est souvent impossible à garantir dans les systèmes distribués. Rust permet d'implémenter deux stratégies :
| Stratégie | Description | Implémentation typique |
|---|---|---|
| Transaction locale (ACID) | Utilisation d’une base relationnelle avec commit/rollback autour d’un agrégat. | diesel::connection::Transactional ou le bloc pool.begin().await?. |
| Eventual consistency | Publication d’événements (Domain Events) après validation de l’agrégat, puis traitement asynchrone. | Créer un DomainEventPublisher (trait) et l’injecter dans les services applicatifs; utilisation de Kafka ou NATS. |
Le type system de Rust peut garantir que les événements ne sont émis qu’après un état valide<0xE2><0x80><0xAF>: la fonction qui confirme une commande retourne un struct OrderConfirmed implémentant DomainEvent.
8️⃣ Tests et validation – tirer parti du typage fort
8.1 Tests unitaires sur le domaine
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
#[test]
fn add_item_respects_ceiling() {
let mut order = Order::new(Uuid::new_v4(), Money::new(dec!(0), Currency::EUR));
let expensive_item = OrderItem::new(
ProductId::new("P123"),
Money::new(dec!(9_999), Currency::EUR),
);
// Première insertion ok
assert!(order.add_item(expensive_item.clone()).is_ok());
// Deuxième insertion dépasse le plafond de 10<0xE2><0x80><0xAF>000 EUR → erreur
let err = order.add_item(expensive_item).unwrap_err();
assert_eq!(err, "Plafond de commande dépassé");
}
}
Le compilateur assure que les préconditions (ex.<0xE2><0x80><0xAF>: devise identique) sont respectées avant l’exécution du test.
8.2 Tests d’intégration avec tokio et base en mémoire
#[tokio::test]
async fn repository_roundtrip() {
let pool = setup_test_db().await;
let repo = DieselOrderRepo { pool };
let order = Order::fixture(); // crée une commande valide
repo.insert(&order).await.unwrap();
let fetched = repo.get(order.id()).await.unwrap().unwrap();
assert_eq!(fetched, order);
}
Ces tests valident que les adapters respectent le contrat du port sans logique métier supplémentaire.
9️⃣ Points de vigilance et limites : ce qu’il faut mesurer avant d’adopter
9.1 Courbe d’apprentissage
Rust possède une syntaxe et un modèle de possession qui demandent plusieurs semaines d’intégration pour des équipes habituées à JavaScript ou Python. La productivité initiale peut chuter de 30 % à 50 % selon les études internes de la communauté Rust (exemple tiré du State of Rust Survey 2023).
9.2 Maturité des bibliothèques DDD
Contrairement à l’écosystème Java, il n’existe pas de framework complet « DDD‑Rust ». Les développeurs doivent composer les abstractions (traits, crates) eux‑mêmes, ce qui augmente la charge de conception et le risque d’incohérences.
9.3 Gestion du borrowing dans les agrégats
Les agrégats contenant des collections mutables peuvent déclencher des conflits d’emprunt (cannot borrow order as mutable because it is also borrowed as immutable). La résolution passe souvent par refactoring (ex. : utilisation de Rc<RefCell<_>> ou du pattern Command), ce qui peut atténuer les garanties d’immutabilité.
9.4 Interopérabilité avec des services externes
Les systèmes existants (micro‑services Java, bases NoSQL) nécessitent souvent des bindings FFI ou des ponts HTTP/GRPC. Le coût de ces interfaçages doit être intégré dans le calcul total du projet.
9.5 Coût d’infrastructure
Rust génère des binaires statiques, ce qui simplifie le déploiement (Docker images < 30 Mo) mais peut augmenter la consommation de RAM lors du démarrage si plusieurs services lourds sont exécutés sur un même nœud.
🔟 Conclusion opérationnelle – Décider d’adopter DDD en Rust
Le
Poursuivre le parcours : Conformité NIS2, DORA et RGPD → Solution → Produit → Demander une démonstration