DEV Community

Cover image for Building EDIFlow in TypeScript: Part 2 - Domain Layer Deep-Dive
hello-ediflow
hello-ediflow

Posted on

Building EDIFlow in TypeScript: Part 2 - Domain Layer Deep-Dive

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
Enter fullscreen mode Exit fullscreen mode

❌ 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)
Enter fullscreen mode Exit fullscreen mode

⚠️ 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
Enter fullscreen mode Exit fullscreen mode

βœ… 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

  1. Entities - objects with identity
  2. 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!)
Enter fullscreen mode Exit fullscreen mode

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 {}
Enter fullscreen mode Exit fullscreen mode

Pros: Simple, clear

Cons: Duplication, hard to extend

❌ Rejected (violates DRY)

Option 2: if/else Everywhere

if (standard === 'EDIFACT') { ... }
Enter fullscreen mode Exit fullscreen mode

Pros: One class

Cons: Breaks OCP, hard to test

❌ Rejected

Option 3: Polymorphic Value Object (Chosen)

class Version {
  private static PATTERNS = { EDIFACT: /.../, X12: /.../ };
}
Enter fullscreen mode Exit fullscreen mode

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 { }
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

Alternative: Result types (functional style):

Version.create('INVALID'); // returns Result<Version, Error>
Enter fullscreen mode Exit fullscreen mode

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:

Community:

Books:


Part 3 is next: Application Layer.

Top comments (0)