Unlocking Domain Primitives: A Front‑End Guide to Cleaner Code

This article explains Domain‑Driven Design's Domain Primitive concept from a front‑end perspective, compares it with VO/DTO/PO, outlines its principles, provides concrete TypeScript examples, and shows how applying DP can improve validation, encapsulation, and testability in real‑world projects.

MoonWebTeam
MoonWebTeam
MoonWebTeam
Unlocking Domain Primitives: A Front‑End Guide to Cleaner Code

Introduction

Domain‑Driven Design (DDD) is a software development methodology that helps teams capture business knowledge and reflect it in code. This article focuses on the concept of Domain Primitive (DP) from a front‑end viewpoint.

DP Basic Concepts

VO, DTO, PO

Before learning DP, it is useful to understand Value Object (VO), Data Transfer Object (DTO) and Persistent Object (PO) as they are related concepts.

class UserVO {
  private username: string;
  private age: number;
  // getters
}

VO is an immutable view object used only for display.

class UserDTO {
  private username: string;
  private age: number;
  // getters and setters
}

DTO is a mutable object that carries data between layers.

class UserPO {
  private id: number;
  private username: string;
  private password: string;
  // getters and setters
}

PO represents a persistent entity that maps directly to the database.

What is DP

A Domain Primitive is the most basic type in a domain, similar to primitive data types in a programming language, but with precise business definitions, self‑validation, and behavior.

DP is an immutable Value Object.

DP has a well‑defined meaning.

DP uses the domain’s native language.

DP can be a minimal building block or part of a complex composition.

DP vs VO

DP extends VO by adding validity checks and domain behavior while remaining side‑effect‑free.

DP and Entity Relationship

Entities have an ID and mutable state, while DP is immutable and focuses on the meaning of a single value. Replacing raw fields with DP reduces repetitive validation code.

Case Study: Applying DP Principles

The three DP principles are:

Make implicit concepts explicit.

Make implicit context explicit.

Encapsulate multi‑object behavior.

Simulated Business Scenario

A registration system collects a user’s name and landline phone number, then rewards the sales representative based on the phone’s area code.

export class UserRegistrationEntity {
  private salesRepEntity: SalesRepEntity;
  private userEntity: UserEntity;

  public async register(name: string, phone: string) {
    this.isVerify(name, phone);
    const areaCode = this.getAreaCode(phone);
    const operatorCode = this.getOperatorCode(phone);
    const { repId } = await this.salesRepEntity.findRep(areaCode, operatorCode);
    return this.userEntity.save({ name, phone, repId });
  }

  private isVerify(name: string, phone: string) { /* validation logic */ }
  private isValidPhoneNumber(phone: string): boolean { return /^0[1-9]{2,3}-?\d{8}$/.test(phone); }
  private getAreaCode(phone) { /* ... */ }
  private getOperatorCode(phone) { /* ... */ }
}

The original implementation mixes validation, database access, and business logic, violating the Single Responsibility Principle and making testing difficult.

Making Concepts Explicit

By wrapping phone numbers and names into DP objects, validation moves into the DP constructor, so downstream code no longer needs to repeat checks.

class PhoneNumberDp {
  private number: string;
  constructor(number: string) { this.isValid(number); this.number = number; }
  public getNumber() { return this.number; }
  public getAreaCode() { /* ... */ }
  public getOperatorCode() { /* ... */ }
  private isValid(number: string) { /* throw on invalid */ }
}

class NameDp {
  private name: string;
  constructor(name: string) { this.isValid(name); this.name = name; }
  public getName() { return this.name; }
  private isValid(name: string) { /* throw on invalid */ }
}

Using these DPs simplifies the registration use‑case:

export class UserRegistration {
  private salesRepEntity: SalesRepEntity;
  private userEntity: UserEntity;

  public async register(name: NameDp, phone: PhoneNumberDp) {
    const areaCode = phone.getAreaCode();
    const operatorCode = phone.getOperatorCode();
    const { repId } = await this.salesRepEntity.findRep(areaCode, operatorCode);
    return this.userEntity.save({ name, phone: phone.getNumber(), repId });
  }
}

Making Context Explicit

Complex concepts such as a gift package can be modeled as a DP that encapsulates fields and related rules.

class GiftInfoDp {
  private info: GiftInfoParams;
  constructor(data: GiftInfoParams) { this.info = data; }
  public isStarted() { const now = Date.now(); return now > this.info.startTime && now < this.info.endTime; }
  public isEnd() { return Date.now() > this.info.endTime; }
  public isAvailable(level: number) { return this.isStarted() && level >= this.info.minLevel; }
  public getStatusMsg() { /* return status string */ }
}

Encapsulating Multi‑Object Behavior

Order creation can aggregate several DPs (ItemDp, CouponDp, MoneyDp) into a single CreateOrderDp, keeping the use‑case clean.

class CreateOrderDp {
  private item: ItemDp;
  private coupon: CouponDp;
  private money: MoneyDp;
  public getSaleMoney() { /* calculate discount */ }
}

function createOrder() {
  const order = new CreateOrderDp({ item: new ItemDp(...), coupon: new CouponDp(...), money: new MoneyDp(1000, MoneyType.CNY) });
  orderEntity.createOrder(order);
}

DP Application Scenarios

Strings with format constraints (Name, PhoneNumber, ZipCode, etc.)

Integers with range limits (OrderId > 0, Percentage 0‑100, Quantity ≥ 0)

Enumerated integers (Status)

Decimals with business meaning (Money, ExchangeRate, Rating)

Complex structures (urlDp, moneyDp, couponDp, loginInfoDp, signatureDp)

DP in Business Refactoring

Experimental Platform (Node)

TabLayerCode is a string that carries sceneId, layerCode and conditionId. By turning it into a TabLayerCodeDp, validation, parsing and generation become methods of the DP, eliminating scattered logic.

class TabLayerCodeDp {
  private tabLayerCode: string;
  private sceneId: string;
  private layerCode: string;
  private conditionId: string;
  constructor(data: TabLayerCodeDpParams) { /* resolve or build code */ }
  public getTabLayerCode() { /* build if missing */ }
  private resolverTabLayerCode(code: string) { /* split and assign */ }
}

The service now accepts TabLayerCodeDp directly, guaranteeing correctness.

H5 Cloud Game

PlayCountDp encapsulates current play count, maximum count, and activity type, providing methods to check availability and generate user‑facing messages.

class PlayCountDp {
  private playCount: number;
  private maxPlayCount: number;
  private activityType: ActivityType;
  constructor(playCount: number, maxPlayCount: number, activityType?: ActivityType) { /* validate */ }
  public canPlay() { return this.playCount <= this.maxPlayCount; }
  public getGameEngMsg() { /* return appropriate message */ }
  public getCountText() { return `${Math.min(this.playCount, this.maxPlayCount)}/${this.maxPlayCount}`; }
}

DP Learning Summary

Avoid overly abstract concepts; keep the model practical.

Define DPs based on business needs, not technical convenience.

Prioritize business‑oriented DP design during tactical DDD.

Prefer using DPs inside Entities to reduce validation code and improve testability.

frontendTypeScriptDomain-Driven DesignDomain Primitive
MoonWebTeam
Written by

MoonWebTeam

Official account of MoonWebTeam. All members are former front‑end engineers from Tencent, and the account shares valuable team tech insights, reflections, and other information.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.