Series: Building EDIFlow - A Clean Architecture Journey in TypeScript (Part 2/6)
Author: EDIFlow
Published: February 2026
Reading Time: ~10-12 minutes
Welcome Back! π
In Part 1, I shared why I built EDIFlow and why Clean Architecture was the right choice.
Now itβs time for the hard part: design decisions.
Today we focus on the Domain Layer β not the implementation details, but the decisions that shaped it.
What youβll learn:
- What objects to model (and what NOT to model)
- Decision criteria for Entity vs Value Object
- Trade-offs that influenced the design
- Why DDD + Clean Architecture belong together
- What you can reuse in your own systems
No perfect solutions β just real decisions with real trade-offs.
1. The First Question: What IS the Domain? π€
This sounds obvious, but itβs the hardest question in Clean Architecture:
βWhat belongs in the Domain Layer?β
Uncle Bob says in Clean Architecture:
βThe Domain Layer contains Enterprise Business Rules. These are the rules that would exist even if the application didn't exist.β
Translation: Domain = pure business logic, independent of technology.
My Challenge with EDIFlow
EDIFlow parses messages. But whatβs the βdomainβ?
First instinct (wrong):
Domain = Parsing logic
β Parsing is technical, not business.
I spent a week building parsers in the domain layer. Big mistake! When I tried to add X12 support, I had to change the domain. That's when I realized: if adding a new format changes your domain, your domain is wrong.
Second try:
Domain = Message structure (segments, elements)
β οΈ Better, but still technical.
Final answer:
Domain = Business concepts that exist independently
- A message has a type
- A message has a version
- A message has validation rules
- A message has identity
β This is the domain. It exists before parsing, after parsing, and independent of any parser.
Key lesson: Don't confuse technical implementation with business concepts. I learned this the hard way!
2. Enter Domain-Driven Design π
Now I knew what belongs in the Domain Layer. But I still had a problem: Which objects do I create?
Clean Architecture tells me where logic belongs. It doesnβt tell me how to model that logic.
Thatβs where Domain-Driven Design (DDD) helps. Eric Evans describes several building blocks for domain modeling:
- Entities - Objects with identity
- Value Objects - Objects defined by value
- Aggregates - Cluster of entities with a root
- Domain Services - Operations that donβt belong to objects
- Repositories - Persistence abstractions
- Factories - Complex object creation
Note: These ideas existed before DDD, but Evans made them clear and practical for domain modeling.
3. Why DDD + Clean Architecture? π
Why do I need both? Because they solve different problems:
| Domain-Driven Design | Clean Architecture | |
|---|---|---|
| Answers | WHAT to model | HOW to structure |
| Question | What objects? What relationships? | Where does code go? |
| Gives you | Entities, Value Objects, Aggregates | Layers, Dependencies, Boundaries |
| Goal | Model business correctly | Structure code maintainably |
Together theyβre powerful:
DDD tells you WHAT:
- EDIMessage is an Entity (identity)
- Version is a Value Object (value-based)
- Standard is an Enum (type-safe set)
Clean Architecture tells you WHERE:
- Entities live in Domain Layer
- Parsers live in Infrastructure Layer
- Use Cases live in Application Layer
Real example (compact):
DDD Decision:
"Version is a Value Object" (immutable, no identity)
Clean Architecture Decision:
"Version lives in domain/value-objects/ with zero dependencies"
Result:
β
Correct model (DDD)
β
Correct structure (Clean Architecture)
β
Portable + testable
Both complement each other: DDD without structure = mess. Structure without domain insight = technical code without business value.
4. Why Focus Only on Entities & Value Objects? π―
DDD has many building blocks, but this article focuses on just two:
- Entities - objects with identity
- Value Objects - objects defined by value
Why only these two?
Honest answer: Because EDIFlowβs Domain Layer is simple:
- β No Aggregates (EDIMessage is simple enough)
- β No Domain Services (operations fit in objects or use cases)
- β No Repositories in Domain (they belong in Infrastructure)
- β No Factories needed (constructors are enough)
YAGNI applies here. Donβt add patterns you donβt need.
Later parts in the series:
- Part 3: Use Cases, Services, DTOs
- Part 4: Repositories, Parsers
5. The Big Decision: Entity or Value Object? π¦
I use a simple decision matrix:
| Question | If YES β | If NO β |
|---|---|---|
| Does it have an ID? | Entity | Value Object |
| Can two be equal with different values? | Entity | Value Object |
| Does it change over time? | Entity | Value Object |
| Do I need to track its history? | Entity | Value Object |
Applied to all EDIFlow domain objects:
| Object | Has ID? | Equal w/ diff values? | Changes? | Track history? | Verdict |
|---|---|---|---|---|---|
| EDIMessage | β | β | β | β | Entity |
| Version | β | β | β | β | Value Object |
| MessageType | β | β | β | β | Value Object |
| Delimiters | β | β | β | β | Value Object |
| Standard | β | β | β | β | Value Object (Enum) |
| EDISegment | β | β | β | β | Value Object |
| EDIElement | β | β | β | β | Value Object |
Pattern: Only EDIMessage has identity. Everything else is a Value Object.
Key insight (1:6 ratio):
- 1 Entity (EDIMessage)
- 6 Value Objects (everything else)
DDD takeaway: Start by assuming Value Object. Only choose Entity if identity truly matters.
See It In Action
Here's how the decision matrix plays out in real code:
// Entity: Has identity, can be equal despite different values
const message1 = new EDIMessage('ORDER-001', version, type, segments);
const message2 = new EDIMessage('ORDER-001', version, type, segments);
message1.equals(message2); // β
true (same ID!)
// Value Object: No identity, equals by value only
const v1 = new Version(Standard.EDIFACT, 'D.96A');
const v2 = new Version(Standard.EDIFACT, 'D.96A');
v1.equals(v2); // β
true (same value!)
const v3 = new Version(Standard.EDIFACT, 'D.01B');
v1.equals(v3); // β false (different value!)
Why this matters: Two orders with same content are different orders (Entity). Two versions "D.96A" are the same version (Value Object).
6. The Hardest Decision: Polymorphism vs Simplicity π
EDIFlow supports multiple formats (EDIFACT, X12, IDOC). Each has different rules.
How do I model this?
I struggled with this for days. Every approach felt wrong:
- Separate classes = duplication
- if/else = messy
- Polymorphism = complex?
Let me show you the three options I considered:
Option 1: Separate Classes (Naive)
class EdifactVersion {}
class X12Version {}
class IdocVersion {}
Pros: Simple, clear
Cons: Duplication, hard to extend
β Rejected (violates DRY)
Option 2: if/else Everywhere
if (standard === 'EDIFACT') { ... }
Pros: One class
Cons: Breaks OCP, hard to test
β Rejected
Option 3: Polymorphic Value Object (Chosen)
class Version {
private static PATTERNS = { EDIFACT: /.../, X12: /.../ };
}
Pros: One class, extensible, type-safe
Cons: Slightly more complex
β Chosen (best trade-off)
7. Trade-Off Example: Validation Strategy π
| Approach | Pros | Cons | Chosen? |
|---|---|---|---|
| External Validator | Flexible | Separate logic | β |
| Self-validating constructor | Fail-fast | Exceptions | β |
| Factory method | Controlled | Extra layer | β οΈ |
Why self-validation: It makes invalid state impossible.
8. What I'd Do Differently π€
Let me be honest about mistakes and second thoughts:
Mistake #1: Almost Over-Engineered Version
What I almost did:
interface IVersion { ... }
class EdifactVersion implements IVersion { }
class X12Version implements IVersion { }
Why I didn't: YAGNI! The polymorphic approach works great. Don't create interfaces until you need them.
Lesson: Resist the urge to make everything "extensible." Simple solutions often stay simple.
Mistake #2: First Draft Had 50+ Classes
My very first attempt had separate classes for everything:
-
EdifactDelimiters,X12Delimiters -
EdifactSegment,X12Segment -
EdifactElement,X12Element
Why that was bad: Massive duplication. 80% of the code was identical.
What I learned: Look for patterns before creating classes. Most Value Objects are format-agnostic.
Thought #1: Should I Use Result Types?
Right now, constructors throw exceptions:
new Version(Standard.EDIFACT, 'INVALID'); // throws!
Alternative: Result types (functional style):
Version.create('INVALID'); // returns Result<Version, Error>
Why I didn't: Most TypeScript developers expect exceptions. Result types are great, but add learning curve.
Would I change it? Maybe in v2.0 with a Version.tryCreate() method. For now, exceptions work fine.
9. What You Get From This Design π
- β Type safety (less guessing)
- β Invalid state impossible
- β Clear separation of concerns
- β Easier testing (pure logic)
- β No breaking changes when adding formats
10. What's Next? π
Part 3: Application Layer
- Use Cases
- Services
- DTOs
- Dependency Injection (no framework)
11. Conclusion π―
Domain design is about decisions:
- What to model
- How to classify objects
- What trade-offs to accept
EDIFlowβs Domain Model:
-
1 Entity:
EDIMessage -
6 Value Objects:
Standard,Version,MessageType,Delimiters,EDISegment,EDIElement
Rule of thumb: Start with Value Object. Promote to Entity only when identity truly matters.
π Series Navigation
Part 1 (Published):
Upcoming Parts:
- Part 3: Application Layer - Use Cases & Services
- Part 4: Infrastructure Layer & Monorepo
- Part 5: Presentation Layer - CLI
- Part 6: Lessons Learned & The Big Refactoring
π¬ Letβs Discuss
How do you decide between Entity and Value Object in your projects?
- Do you prefer decision matrices like this?
- What trade-offs do you value most (simplicity, extensibility, strictness)?
- Have you used DDD + Clean Architecture together?
Iβd love to hear your approach β drop your thoughts in the comments.
π Links & Resources
EDIFlow:
- π¦ npm: @ediflow/core
- π GitHub: ediflow-lib/core
- π Architecture Docs
Community:
- π¬ Discord: EDIFlow Community
- π£ GitHub Discussions
Books:
- π Clean Architecture by Robert C. Martin
- π Domain-Driven Design by Eric Evans
- π Clean Code by Robert C. Martin
Part 3 is next: Application Layer.
Top comments (0)