Carbonteq
Best Practices/Backend/Architecture/Domain

Overview

Entity Evolution: Building a Robust Host Entity

This section demonstrates the step-by-step evolution of a Host entity from basic TypeScript patterns to a robust, type-safe implementation using functional programming principles.

1
Foundation - Interface and Basic Class

BaseEntity provides id, createdAt, updatedAt and serialization helpers. This step defines IHost interface and Host class to describe the host entity structure.

2
Adding core Properties with refined types

Refined types ensure userId is a valid UUID and address is a structured value object. This step adds userId, dob, phoneNumber, address, socialLinks, and profileImage to the Host class.

3
Making Properties Optional with Option<T>

Option<T> makes missing values explicit and type-safe. This step wraps dob, phoneNumber, and profileImage in Option to handle optional host data.

4
Empty Constructor Setup

Constructors accept raw input and initialize BaseEntity fields via super(). This step adds an empty constructor that will be progressively filled with validation and assignment logic.

5
Adding Computed Properties

hasCompleteProfile checks if required profile fields are present for business rules. This step returns true when dob, phoneNumber, and profileImage all contain values.

6
Creating SerializedHost Type

SerializedHost represents the entity format for external systems and databases. This step defines a type with string id, nullable dates, and simplified value objects. Learn more: Type Boundaries

7
Introducing Maybe<T> Utility Type

Maybe<T> accepts T | Option<T> | null | undefined from different data sources. This step replaces verbose unions with a single Maybe<T> type alias.

8
Adding normalizeMaybe Utility

normalizeMaybe converts Maybe<T> to Option<T> at entity boundaries to keep domain logic uniform. This step handles null, undefined, existing Option, and raw values in one function.

9
Implementing Serialize Method

serialize converts internal Option<T> values back to external Maybe<T> format. This step uses optionToMaybe to transform Host fields for APIs and databases.

10
Adding optionToMaybe Utility

optionToMaybe centralizes the Option<T> to Maybe<T> transformation logic. This step extracts the helper to reduce duplication across serialize methods.

11
Adding Basic Guards to Class

Guards ensure field values meet business rules before entity creation. This step adds validatePhoneNumber and validateDateOfBirth as private static methods.

12
Using Guards in Constructor

Constructor validation prevents invalid entities from being created. This step normalizes inputs then runs validatePhoneNumber and validateDateOfBirth before assignment.

13
Making Constructor Private with Factory Method

Private constructors prevent unvalidated entity creation via new. This step adds Host.create factory method that always runs validation before instantiation.

14
Moving Guards to Separate File

Separate guard files enable reuse across domain and application layers. This step moves validatePhoneNumber and validateDateOfBirth into HostGuards class.

15
Extracting Utility Types to Separate File

Shared utilities prevent duplication across multiple entities. This step moves Maybe<T>, normalizeMaybe, and optionToMaybe into domain/utils/utils.ts.

16
Fixing the Messy Guards in Class

Factory methods compose normalizeMaybe with HostGuards validators and throw HostValidationError. This step replaces inline validation with external guard calls.

17
The Problem with Manual Encoding/Decoding

Manual serialization creates repetitive code, type gaps, and exception-based errors. This step lists the issues we will solve with Effect Schema.

18
Say Goodbye to Manual Serialization - Introducing Effect Schema

Effect Schema derives types and validation from a single definition. This step shows PersonSchema example that automatically generates types, validation, and transformations.

19
Creating Host Schema

HostSchema replaces manual types and validation with declarative field definitions. This step defines schema with StringToUUID, Optional fields, and composed value object schemas.

20
Deriving Types from Schema

Schema.Type and Schema.Encoded automatically generate runtime and serialized types. This step eliminates manual HostType and SerializedHost definitions by deriving them from HostSchema.

21
Integrating Guards into Schema

Schema.pipe integrates validation directly into field definitions. This step adds HostGuards.ValidDateOfBirth, ValidPhoneNumber, and ValidProfileImagePath to schema fields.

22
Simplified Entity with Effect Schema

Schema-derived types eliminate manual Option conversions in constructors. This step assigns HostType fields directly without normalizeMaybe or type casting.

23
Effect-Based Factory Method

Effect<Host, HostError, never> returns typed errors instead of throwing exceptions. This step composes S.decodeUnknown, guards, and value object creation in a single pipeline.

24
Simplified Serialization

S.encode(HostSchema) serializes entities without manual field mapping. This step replaces the multi-line serialize method with a single Effect-returning call.

import { BaseEntity, IEntity } from "@domain/utils/base.entity";
import { UUID } from "@domain/utils/refined.types";
import { Address } from "./address.vo";
import { SocialLinks } from "./socialLinks.vo";
interface IHost extends IEntity {
userId: UUID;
dob: Date;
phoneNumber: number;
profileImage: string;
}
export class Host extends BaseEntity implements IHost {
// Properties will be added in the next step
}

Code Organization

The domain layer is organized by business entities and utilities, following domain-driven design principles:

user.entity.ts
user.guards.ts
user.error.ts
host.entity.ts
address.vo.ts
base.entity.ts
base.repository.ts
base.errors.ts
refined.types.ts
validation.utils.ts
pagination.ts
index.ts

Core Utilities Explained

1. Base Entity (base.entity.ts)

Provides foundational entity functionality with Effect support:

  • BaseEntity class: Abstract base with id, createdAt, updatedAt
  • IEntity interface: Core entity contract
  • SerializedEntity: Type for persistence layer
  • _fromSerialized(): Safe entity reconstruction from data
  • _serialize(): Convert entity to plain object

2. Refined Types (refined.types.ts)

Branded types with Effect Schema integration:

  • UUID: Branded UUID type with validation
  • DateTime: Branded DateTime with coercion
  • Email: Validated email addresses
  • StringToUUID: Effect Schema transformer for UUID
  • DateTimeFromAny: Union transformer for multiple date formats
  • Optional(): Helper for optional fields with null handling

3. Validation Utils (validation.utils.ts)

Reusable validation building blocks:

  • createNotEmptyFilter(): Factory for non-empty string validation
  • Optional(): Wrapper for nullable schema fields

4. Base Repository (base.repository.ts)

Abstract repository pattern with Effect:

  • BaseRepository<T>: Generic repository base class
  • RepositoryEffect<T, E>: Type alias for Effect-based operations
  • Common CRUD operations: insert, update, fetchById, etc.

5. Base Errors (base.errors.ts)

Domain error hierarchy:

  • DomainError: Base error class
  • ValidationError: For validation failures
  • NotFoundError: For missing entities
  • AlreadyExistsError: For duplicate entities

6. Pagination (pagination.ts)

Effect-based pagination utilities:

  • PaginationOptions: Validated pagination parameters
  • Paginated<T>: Interface for paginated results
  • Paginator: Utility for paginating collections