ddd-value-object-immutability
Report value-object classes whose instance fields can be reassigned after construction.
Report a value-object class whose instance fields can be reassigned. A value object is defined by its value; if it can change after construction, two holders of "the same" value drift apart.
This rule is part of eslint-plugin-modularity.
Quick Summary
| Aspect | Details |
|---|---|
| Severity | Error (code quality) |
| Auto-Fix | ❌ No |
| Category | Modularity |
| Suggestions | ✅ Add readonly, freeze in the constructor |
Rule Details
A class is a value object when its name ends with one of valueObjectPatterns
(default Value, VO, ValueObject). The rule reports such a class when it
declares instance state without readonly:
- a class field (
amount: number;); - a constructor parameter property (
constructor(public amount: number)), which declares the same field.
Static fields are class state, not the value of an instance, and are not checked.
It does not report a value object that:
- declares only
readonlyfields andreadonlyparameter properties; - calls
Object.freeze(this)in its constructor (whilecheckObjectFreezeis true). Freezing a nested value,Object.freeze(this.items), leaves the instance's own fields writable and does not count.
The check is shallow: a readonly items: string[] field passes, though the
array itself stays mutable.
Configuration
| Option | Type | Default | Description |
|---|---|---|---|
valueObjectPatterns | string[] | ['Value', 'VO', 'ValueObject'] | Class-name suffixes that mark a value object |
requireReadonly | boolean | true | Report fields without readonly. false turns the check off |
checkObjectFreeze | boolean | true | Accept Object.freeze(this) in the constructor |
Examples
❌ Incorrect
class MoneyValueObject {
amount: number;
constructor(amount: number, public currency: string) {
this.amount = amount;
}
}✅ Correct
class MoneyValueObject {
readonly amount: number;
constructor(amount: number, private readonly currency: string) {
this.amount = amount;
}
}
class PriceVO {
amount = 0;
constructor() {
Object.freeze(this);
}
}Configuration Examples
// eslint.config.mjs
export default [
{
rules: {
'modularity/ddd-value-object-immutability': ['error', { valueObjectPatterns: ['Money', 'VO'] }],
},
},
];Related Rules
ddd-anemic-domain-model- Entities should carry behaviour
Use this rule
npm install -D oxlint-plugin-modularity{ "jsPlugins": ["oxlint-plugin-modularity"], "rules": { "modularity/ddd-value-object-immutability": "error" }}import { defineConfig } from 'vite-plus';export default defineConfig({ lint: { jsPlugins: ['oxlint-plugin-modularity'], rules: { 'modularity/ddd-value-object-immutability': 'error', }, },});oxlint-plugin-modularity re-exports eslint-plugin-modularity/oxlint: the same rules under the same IDs, and no ESLint install. Why oxlint.
Did this rule catch something? Star the repo to get new CWE coverage as we ship it — or follow the AI-code-security benchmarks behind these rules.