Domain - Couche Domaine (Core Métier)
La couche Domain contient la logique métier pure, complètement indépendante des frameworks et technologies.
Principe : Le Domain ne dépend de PERSONNE. Zéro dépendance externe (sauf crates utilitaires : uuid, chrono, serde).
Structure
domain/
├── entities/ # Entités métier avec invariants
│ ├── user.rs
│ ├── organization.rs
│ ├── building.rs
│ ├── unit.rs
│ ├── owner.rs
│ ├── expense.rs
│ ├── meeting.rs
│ ├── document.rs
│ └── refresh_token.rs
└── services/ # Services domaine
├── expense_calculator.rs
├── pcn_mapper.rs
└── pcn_exporter.rs
Entities (Entités Métier)
Les entités encapsulent les règles métier et garantissent les invariants.
- backend/src/domain/entities/building.rs
- backend/src/domain/entities/unit.rs
- backend/src/domain/entities/owner.rs
- backend/src/domain/entities/expense.rs
- backend/src/domain/entities/meeting.rs
- backend/src/domain/entities/document.rs
- backend/src/domain/entities/user.rs
- backend/src/domain/entities/organization.rs
- Refresh Token Entity
Caractéristiques Communes :
✅ Identifiant UUID unique
✅ Timestamps
created_at/updated_at✅ Validation dans constructeur
new()✅ Méthodes métier (ex:
Building::update_info())✅ Tests unitaires in-module
Exemple Pattern :
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Building {
pub id: Uuid,
pub name: String,
// ... autres champs
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl Building {
pub fn new(...) -> Result<Self, String> {
// Validation invariants métier
if name.is_empty() {
return Err("Building name cannot be empty".to_string());
}
Ok(Self {
id: Uuid::new_v4(),
name,
created_at: Utc::now(),
updated_at: Utc::now(),
// ...
})
}
pub fn update_info(&mut self, ...) {
// Logique métier
self.updated_at = Utc::now();
}
}
#[cfg(test)]
mod tests {
#[test]
fn test_create_building_success() { ... }
#[test]
fn test_create_building_empty_name_fails() { ... }
}
Relations Entre Entités
Organization (Multi-tenant)
│
├──> Users (1:N)
│
└──> Buildings (1:N)
│
├──> Units (1:N)
│ │
│ └──> Owners (N:1)
│
├──> Expenses (1:N)
│
├──> Meetings (1:N)
│
└──> Documents (1:N)
Domain Services
Services domaine pour logique métier complexe impliquant plusieurs entités.
ExpenseCalculator :
Calcule la répartition des charges selon les quotes-parts.
impl ExpenseCalculator {
pub fn calculate_share(
expense: &Expense,
unit: &Unit,
total_shares: i32
) -> f64 {
(expense.amount as f64 * unit.ownership_share as f64)
/ total_shares as f64
}
}
PCNMapper :
Mappe les données pour génération Précompte de Charge Notariale (PCN).
PCNExporter :
Exporte PCN en PDF ou Excel.
Validation Métier
Toute validation est dans les entités :
// ✅ BON : Validation dans l'entité
impl Building {
pub fn new(name: String, total_units: i32, ...) -> Result<Self, String> {
if name.is_empty() {
return Err("Name cannot be empty".to_string());
}
if total_units <= 0 {
return Err("Total units must be > 0".to_string());
}
// ...
}
}
// ❌ MAUVAIS : Validation dans le handler HTTP
async fn create_building(req: HttpRequest) -> Result<HttpResponse> {
if dto.name.is_empty() { // NON ! Ceci appartient au domain
return Err(...)
}
}
Règles DDD Appliquées
Ubiquitous Language :
Terminologie métier : Building (immeuble), Owner (copropriétaire), Unit (lot), Expense (charge)
Aggregates :
Building : Aggregate root
Units : Entités de l’aggregate Building
Règle : Modification Units passe toujours par Building
Value Objects (à implémenter) :
pub struct Address { street: String, city: String, postal_code: String, country: String, } pub struct Email(String); // Email valide
Domain Events (futur) :
pub enum BuildingEvent { BuildingCreated { id: Uuid, name: String }, BuildingUpdated { id: Uuid }, BuildingDeleted { id: Uuid }, }
Tests Domaine
Objectif : 100% coverage domaine (logique critique)
# Tests unitaires domaine uniquement
cargo test --lib domain::
# Tests entité spécifique
cargo test --lib domain::entities::building
Pattern de Test :
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_building_success() {
let building = Building::new(
"Test Building".to_string(),
"Address".to_string(),
"City".to_string(),
"12345".to_string(),
"Country".to_string(),
10,
None,
);
assert!(building.is_ok());
let building = building.unwrap();
assert_eq!(building.name, "Test Building");
assert_eq!(building.total_units, 10);
}
#[test]
fn test_create_building_empty_name_fails() {
let building = Building::new(
"".to_string(), // Invalide
"Address".to_string(),
"City".to_string(),
"12345".to_string(),
"Country".to_string(),
10,
None,
);
assert!(building.is_err());
assert_eq!(
building.unwrap_err(),
"Building name cannot be empty"
);
}
}
Dépendances Autorisées
Crates externes autorisées dans le Domain :
[dependencies]
uuid = { version = "1.11", features = ["v4", "serde"] }
chrono = { version = "0.4", features = ["serde"] }
serde = { version = "1.0", features = ["derive"] }
Interdictions :
❌ Pas d’Actix-web (framework web)
❌ Pas de SQLx (base de données)
❌ Pas de Tokio async (sauf si absolument nécessaire)
❌ Pas de dépendances vers Application ou Infrastructure
Évolutions Futures
Value Objects :
pub struct Email(String); pub struct PhoneNumber(String); pub struct Address { ... }
Domain Events :
pub trait DomainEvent { fn event_type(&self) -> &str; fn aggregate_id(&self) -> Uuid; fn occurred_at(&self) -> DateTime<Utc>; }
Specifications Pattern :
pub trait Specification<T> { fn is_satisfied_by(&self, entity: &T) -> bool; } pub struct PaidExpenseSpecification; impl Specification<Expense> for PaidExpenseSpecification { fn is_satisfied_by(&self, expense: &Expense) -> bool { expense.payment_status == PaymentStatus::Paid } }
Factory Pattern :
pub struct BuildingFactory; impl BuildingFactory { pub fn create_residential(...) -> Result<Building, String> { ... } pub fn create_commercial(...) -> Result<Building, String> { ... } }
Références
Domain-Driven Design (Eric Evans)
Implementing Domain-Driven Design (Vaughn Vernon)
Clean Architecture (Robert C. Martin)