Le problème
Dans beaucoup de codebases, presque tout devient une entité.
On crée des classes ou des types pour :
- une adresse
- un montant
- une période
- un email
- un pourcentage
- un nom
- un identifiant fiscal
Puis on leur ajoute :
- un
id - de la mutabilité
- des méthodes de persistance implicites
- des cycles de vie artificiels
Résultat :
- le modèle devient inutilement lourd
- des objets simples deviennent compliqués à comparer
- des invariants basiques restent dispersés
- le code manipule trop de structures “vivantes” alors qu’il manipule souvent juste des valeurs
L’erreur inverse existe aussi : traiter comme une simple valeur quelque chose qui a une vraie identité métier et un cycle de vie propre.
La distinction entre entity et value object sert à éviter ça.
L’idée simple
La différence utile est simple :
- une entity est définie par son identité
- un value object est défini par sa valeur
Autrement dit :
- si deux objets représentent la même chose parce qu’ils ont le même identifiant, on est souvent face à une entity
- si deux objets sont équivalents parce qu’ils portent exactement les mêmes données utiles, on est souvent face à un value object
Exemples simples :
- un
Customer, uneOrder, uneInvoicesont souvent des entities - une
Money, uneEmailAddress, uneDateRange, unePostalAddresssont souvent des value objects
Le point important est que cette distinction aide à mieux modéliser le métier. Ce n’est pas un exercice de vocabulaire.
Comment ça fonctionne
1. Une entity garde son identité dans le temps
Une entity peut changer sans cesser d’être la même chose.
Exemple :
- un client change d’email
- une commande change de statut
- une facture passe de
draftàpaid
On considère toujours qu’il s’agit du même objet métier.
Ce qui reste stable n’est pas forcément sa donnée complète. C’est son identité.
2. Un value object est défini par ce qu’il contient
Un value object n’existe pas pour être suivi individuellement dans le temps. Il représente une valeur métier.
Exemples :
Money(100, "EUR")EmailAddress("alice@example.com")DateRange("2026-04-10", "2026-04-15")
Si deux value objects ont exactement la même valeur utile, ils sont interchangeables dans le modèle.
On ne se demande pas : “est-ce le même objet historique ?” On se demande : “est-ce la même valeur métier ?”
3. Les value objects sont souvent un bon endroit pour les invariants simples
Un value object est très utile pour encapsuler des règles locales.
Exemples :
- un email doit avoir un format valide
- un montant ne doit pas être négatif
- une période doit avoir une date de fin après la date de début
- une devise doit faire partie d’un ensemble connu
Plutôt que de laisser ces règles dispersées partout, on peut les concentrer dans le value object.
Schéma
Lecture utile :
- une entity vit dans le temps
- un value object porte surtout une signification par sa valeur
- les deux ont des rôles différents dans le modèle
Exemple concret
Prenons un système de facturation.
On manipule :
- une facture
- un email client
- un montant
- une période de facturation
Facture comme entity
Une facture :
- a une identité
- change de statut
- peut être payée
- peut être annulée
- est suivie dans le temps
export class Invoice {
constructor(
public readonly id: string,
private status: "draft" | "issued" | "paid" | "cancelled",
private customerEmail: EmailAddress,
private total: Money
) {}
issue(): void {
if (this.status !== "draft") {
throw new Error("Only a draft invoice can be issued");
}
this.status = "issued";
}
markAsPaid(): void {
if (this.status !== "issued") {
throw new Error("Only an issued invoice can be paid");
}
this.status = "paid";
}
}
Ici, Invoice est clairement une entity.
Email et Money comme value objects
export class EmailAddress {
constructor(public readonly value: string) {
if (!value.includes("@")) {
throw new Error("Invalid email address");
}
}
equals(other: EmailAddress): boolean {
return this.value === other.value;
}
}
export class Money {
constructor(
public readonly amount: number,
public readonly currency: string
) {
if (amount < 0) {
throw new Error("Amount cannot be negative");
}
}
add(other: Money): Money {
if (this.currency !== other.currency) {
throw new Error("Currency mismatch");
}
return new Money(this.amount + other.amount, this.currency);
}
equals(other: Money): boolean {
return this.amount === other.amount && this.currency === other.currency;
}
}
Ici :
EmailAddressest défini par sa valeurMoneyaussi- on ne suit pas leur cycle de vie comme celui d’une facture
- ils servent à rendre le modèle plus précis
Mauvais exemple fréquent
Une erreur classique consiste à transformer un simple concept de valeur en pseudo-entité.
export class Address {
constructor(
public readonly id: string,
public street: string,
public city: string,
public zipCode: string
) {}
}
Ce modèle pose plusieurs questions inutiles :
- pourquoi
Addressa-t-elle une identité propre ici - a-t-on vraiment besoin de suivre cette adresse comme une chose indépendante
- est-ce qu’on compare deux adresses par
idou par contenu
Dans beaucoup de cas, une adresse est mieux modélisée comme value object.
export class PostalAddress {
constructor(
public readonly street: string,
public readonly city: string,
public readonly zipCode: string
) {
if (!street || !city || !zipCode) {
throw new Error("Address is incomplete");
}
}
equals(other: PostalAddress): boolean {
return (
this.street === other.street &&
this.city === other.city &&
this.zipCode === other.zipCode
);
}
}
C’est plus simple et souvent plus juste.
Comment reconnaître une entity
Quelques questions aident beaucoup.
L’objet a-t-il une identité métier propre
Exemples :
- commande
- facture
- abonnement
- réservation
- client
Si oui, on est souvent côté entity.
L’objet change-t-il dans le temps tout en restant “le même”
Si oui, l’identité est probablement centrale.
Le métier a-t-il besoin de suivre son cycle de vie
Si l’objet passe par plusieurs états ou décisions importantes, c’est souvent un bon signal.
Comment reconnaître un value object
Le concept est-il surtout défini par ses données
Exemples :
- montant
- période
- adresse email
- pourcentage
- distance
- coordonnées
Deux instances avec les mêmes données sont-elles interchangeables
Si oui, on est souvent sur un value object.
Peut-on le rendre immuable sans gêner le métier
C’est souvent un très bon signal.
Pourquoi cette distinction compte en pratique
Quand on modélise mieux :
- les entités portent les décisions et cycles de vie importants
- les value objects encapsulent des règles locales utiles
- les comparaisons deviennent plus claires
- le modèle devient moins verbeux
- les invariants simples arrêtent de fuiter partout
C’est un gain de lisibilité et de robustesse, pas seulement de style.
Exemple d’utilisation ensemble
export class Subscription {
constructor(
public readonly id: string,
private readonly customerEmail: EmailAddress,
private readonly monthlyPrice: Money
) {}
getCustomerEmail(): EmailAddress {
return this.customerEmail;
}
getMonthlyPrice(): Money {
return this.monthlyPrice;
}
}
Ici :
Subscriptionest une entityEmailAddressetMoneysont des value objects- le modèle exprime mieux les responsabilités de chacun
Points importants
- Une entity est définie par son identité.
- Un value object est défini par sa valeur.
- Les value objects sont souvent un bon endroit pour des invariants simples.
- Tous les concepts métier n’ont pas besoin d’un
id. - L’immuabilité est souvent un bon choix pour les value objects.
- Cette distinction aide à simplifier le modèle.
Erreurs fréquentes
Donner un id à tout
C’est souvent une trace du schéma SQL ou d’un réflexe technique, pas un vrai besoin métier.
Faire des value objects mutables
Ça complique inutilement le raisonnement et augmente les effets de bord.
Comparer des value objects par identité mémoire
Ce qui compte est leur valeur utile, pas leur emplacement mémoire.
Traiter comme value object quelque chose qui a un vrai cycle de vie
Par exemple une facture, une réservation ou un abonnement. Dans ce cas, on perd la notion d’identité métier.
Laisser des primitives partout
Utiliser directement string, number ou Date partout semble simple, mais laisse souvent les règles locales se disperser.
Conclusion
La distinction entre entity et value object aide à mieux représenter le métier.
Les entities portent une identité et un cycle de vie. Les value objects portent une valeur et des invariants locaux.
Le réflexe utile à retenir est simple :
si le concept compte pour ce qu’il est dans le temps, pense entity ; s’il compte pour ce qu’il vaut, pense value object.