The philosophy is language-neutral; the vocabulary is not. Each ecosystem has its own name for a value object, its own way to express a collection, and its own tool to prove that dependencies point inwards. Five languages are worked through in detail; the rules are the same in all of them.
The same concepts, four vocabularies
| Concept | PHP | C# | Java | C++ | |
|---|---|---|---|---|---|
| Value object | final class + private constructor | record or sealed class | record | class + operator== | class + private constructor |
| First-class collection | class wrapping array | class wrapping IReadOnlyList<T> | class wrapping List<T> | class wrapping std::vector<T> | class wrapping readonly T[] |
| Contract | interface | interface | interface | abstract class, pure virtual | interface |
| Domain failure | exception extending DomainException | exception, or Result<T> | unchecked exception | exception, or std::expected | exception, or a Result<T> union |
| Package boundary | Composer package | NuGet package / project | Maven module | CMake target / Conan package | workspace package (npm/pnpm) |
PHP
The reference implementation, and the language where OTAKU was first applied. Attributes replace annotations; declare(strict_types=1) is mandatory; a class never closes the PHP tag.
- Value objects:
final class, private constructor, named constructors. - Collections: a class with an array field and behaviour — never a bare
arrayin a signature. - Ports: interfaces in the domain namespace; adapters in
Infrastructure. - Tooling: PHP_CodeSniffer, PHPStan/Psalm, deptrac, PHPUnit, Infection.
C#
C# gives you record, init and required, which make value objects almost free. The risk is the opposite of PHP: it is easy to make everything a record and lose behaviour.
- Value objects:
sealed recordwith a validating constructor, or asealed classwithIEquatable<T>. - Collections: wrap
IReadOnlyList<T>; expose behaviour, notIEnumerable. - Ports: interfaces in the domain project; adapters in an infrastructure project with no reference back.
- Tooling: Roslyn analyzers, StyleCop, NetArchTest,
dotnet test.
Java
record covers the value object case; the hard part is module boundaries, because Maven’s default is a single classpath where everything sees everything.
- Value objects:
recordwith a compact constructor that validates. - Collections: wrap
List<T>and never return the internal list. - Ports: interfaces in the domain package; ArchUnit enforces that the domain package imports nothing from infrastructure.
- Tooling: Checkstyle, PMD, ArchUnit,
jdeps, JUnit 5.
C++
The most honest language about costs, and the strictest about ownership. There is no reflection and no framework to hide behind — which makes OTAKU easier to apply and harder to fake.
- Value objects: class with private state and
operator==defaulted; pass by value orconst&. - Collections: wrap
std::vector<T>; expose iterators only if the invariant survives. - Ports: abstract class with pure virtual methods and a virtual destructor; no implementation in the header.
- Tooling: clang-tidy, include-what-you-use, CMake target visibility, sanitizers,
std::expectedfor expected failures.
TypeScript
TypeScript is the fifth language of the guide for a practical reason: it is where most object-oriented code is written today, and it has everything the philosophy needs — classes, interfaces, generics, exceptions and a package manager. The language also makes two rules easy to break, so they need attention.
- Value objects: a class with a private constructor and static factory, because
readonlyalone does not validate. - Collections: wrap
readonly T[]; never return the internal array. - Contracts:
interfacefor ports; keep infrastructure types out of domain signatures. - Pitfall — structural typing: two identical shapes are interchangeable. Add a brand (
declare const brand: unique symbol) when identity matters. - Pitfall —
any: it is the local equivalent ofmixed. Rule 38 applies: only at explicit serialization boundaries. - Tooling: ESLint
complexityandmax-lines-per-function,tsc --strict,ts-archfor dependency rules, Vitest for tests.
What does not translate
- Annotations/attributes have no equivalent in C++; in the other three they are infrastructure and belong outside the domain.
- Garbage collection vs ownership changes how aggregates are copied but not how they are designed.
- Exceptions vs result types changes the signature of failure, not the rule that failure must be explicit.
- Package managers differ in how an assembly is declared; the concept of “no logic, only dependencies” does not.
If your language can express a class, an interface and a package, the philosophy applies. If it cannot, say so — and do not pretend.