Skip to content

Domain Layer

The Domain Layer is the heart of 2KRIKA's clean architecture implementation. It contains the core business logic, entities, and rules that define what the application does, independent of how it does it.

Overview

Located in the domain/ directory, this layer:

  • Has zero external dependencies (no framework imports)
  • Defines business entities as pure TypeScript classes
  • Declares repository interfaces without implementation details
  • Encapsulates business rules within entity methods
  • Establishes domain contracts that outer layers must fulfill

Entities

Entities are the core building blocks representing business concepts with identity and lifecycle.

Entity Characteristics

  1. Identity: Each entity has a unique identifier
  2. Encapsulation: Business rules are methods on the entity
  3. Immutability preference: Where possible, entities favor immutable operations
  4. Framework independence: Pure TypeScript with no decorators

Example: User Entity

// domain/user/user.entity.ts
export class User {
  constructor(
    public id: string,
    public fullName: string,
    public username: string,
    public email: string,
    public passwordHash: string,
    public isActive: boolean = false,
    public isStaff: boolean = false,
    public isSuperuser: boolean = false,
    public title: string = "",
    public description: string = "",
    public notes: string = "",
    public lastLogin: Date | null = null,
    public isKYCVerified: boolean = false,
    public createdAt: Date,
    public updatedAt: Date,
  ) {}

  static create(
    id: string,
    fullName: string,
    username: string,
    email: string,
    passwordHash: string,
    // ... other parameters
  ) {
    return new User(
      id,
      fullName,
      username,
      email,
      passwordHash,
      // ... defaults
    );
  }

  // Business rule: activation
  activate() {
    this.isActive = true;
  }

  // Business rule: deactivation
  deactivate() {
    this.isActive = false;
  }

  // Business rule: permission checking
  hasPermission(permission: Permission): boolean {
    if (this.isSuperuser) {
      return true;
    }
    if (this.permissions) {
      return this.permissions.some((p) => p === permission);
    }
    return false;
  }
}

Key points: - Static factory method create() provides a clear API for instantiation - Business rules like activate() and hasPermission() are encapsulated - No framework dependencies (no @Injectable(), no database decorators)

Example: Order Entity

// domain/orders/order.entity.ts
export class Order {
  constructor(
    public id: string,
    public sellerId: string,
    public customerId: string,
    public serviceId: string,
    public serviceTitle: string,
    public servicePrice: Decimal,
    public serviceDeliveryTime: number,
    public selectedOptions: Option[],
    public totalAmount: Decimal,
    public commissionAmount: Decimal,
    public paymentId: string,
    public state: State,
    public completionDate: Date | null,
    public createdAt: Date,
    public updatedAt: Date,
    public events: Event[],
    public notes: string = "",
  ) {}

  // Business logic: calculate total price
  getTotalPrice(): Decimal {
    return this.totalAmount;
  }

  // Business logic: calculate delivery time
  getDeliveryTime(): number {
    let totalTime = this.serviceDeliveryTime;
    this.selectedOptions.forEach((option) => {
      totalTime += option.time;
    });
    return totalTime;
  }

  // Business rule: check if order is in progress
  isInProgress(): boolean {
    return ONGOING_STATES.includes(this.state);
  }

  // Domain event tracking
  recordEvent(type: EventType) {
    this.events.push({
      timestamp: new Date(),
      type,
    });
  }
}

Key points: - Complex calculations (price, delivery time) are encapsulated - State management uses domain-defined types - Event history is tracked within the entity

Example: Service Entity

// domain/service/service.entity.ts
export class Service {
  constructor(
    public id: string,
    public title: string,
    public slug: string,
    public description: string,
    public status: ServiceStatus, // 'pending' | 'rejected' | 'suspended' | 'active' | 'paused'
    public categoryId: string,
    public ownerId: string,
    public basicPrice: Decimal,
    public basicDeliveryTime: number,
    public reviewsCount: number,
    public averageRating: number,
    public salesCount: number,
    public notes: string,
    public createdAt: Date,
    public updatedAt: Date,
  ) {}

  activate() {
    this.status = "active";
  }

  isActive() {
    return this.status === "active";
  }
}

Value Objects

Value Objects are immutable objects that represent a concept with no identity—they are defined by their attributes.

Example: Permission Value Object

// domain/user/permission.vo.ts
export enum Permission {
  VIEW_ME = "view_me",
  CREATE_USER = "create_user",
  DELETE_USER = "delete_user",
  CHANGE_USER = "change_user",
  VIEW_SERVICE = "view_service",
  // ... many more permissions
  APPROVE_KYC = "approve_kyc",
  REJECT_KYC = "reject_kyc",
}

Key points: - Represented as an enum for type safety - No identity—two permissions with the same value are identical - Used for authorization checks throughout the domain

Example: Option Value Object

// Embedded in Order entity
type Option = {
  title: string;
  price: Decimal;
  time: number;
};

Repository Interfaces

The Domain Layer defines what repositories should do, not how they do it.

Abstract Repository Base

// domain/shared/repository/base.ts
export abstract class AbstractRepository<
  T extends Identifiable<ID>,
  ID = string,
  F = any,
> {
  abstract add(item: T, opts?: any): Promise<T>;
  abstract bulkAdd(items: T[], opts?: any): Promise<T[]>;
  abstract save(item: T, opts?: any): Promise<void>;
  abstract get(id: ID): Promise<T>;
  abstract getBy(criteria: Partial<T>): Promise<T>;
  abstract update(id: ID, data: Partial<T>, opts?: any): Promise<T>;
  abstract delete(id: ID, opts?: any): Promise<void>;
  abstract deleteBy(criteria: Partial<T>, opts?: any): Promise<number>;
  abstract bulkDelete(ids: ID[], opts?: any): Promise<void>;
  abstract deleteAll(opts?: any): Promise<void>;
  abstract all(filters: any & Pagination): Promise<Paginated<T>>;
  abstract filter(criteria: Partial<T>): Promise<T[]>;
  abstract exists(criteria: Partial<T> | any): Promise<boolean>;
  abstract count(filters?: F): Promise<number>;
  abstract nextId(): ID;
  abstract begin(): Promise<Transaction>;
}

export interface Identifiable<ID> {
  id: ID;
}

Key points: - Generic type T represents any entity with an ID - Filter type F allows domain-specific query policies - Methods are abstract—no implementation details - Transaction support via begin()

Domain-Specific Repositories

// domain/user/user.repository.ts
export abstract class UserRepository extends AbstractRepository<User, string, UserQueryPolicy> {
  abstract approveKyc(id: string, opts?: any): Promise<void>;
  abstract getKYCValidationDate(userId: string): Promise<Date | null>;
  abstract filterByIds(ids: string[]): Promise<User[]>;
}

export interface UserQueryPolicy {
  search?: string;
  isActive?: boolean;
  isKYCVerified?: boolean;
  roles?: string[];
  page?: number;
  pageSize?: number;
  orderBy?: string[];
}

Key points: - Extends base repository with user-specific methods - UserQueryPolicy defines allowed filter parameters - Implementation details (SQL queries, joins) are hidden

Domain Events

Domain events represent significant occurrences within the domain.

Event Types

// domain/orders/order.entity.ts
type EventType =
  | "payment.completed"
  | "order.accepted"
  | "order.rejected"
  | "order.delivered"
  | "order.completed"
  | "dispute.resolved"
  | "order.cancel_accepted"
  | "order.cancel_rejected"
  | "order.requires_correction"
  | "correction_request.accepted"
  | "correction_request.declined"
  | "order.cancel_requested";

export type Event = {
  timestamp: Date;
  type: EventType;
};

Key points: - Events are tracked in the Order entity - Used for audit trails and workflow transitions - Immutable once recorded

Domain Exceptions

Domain-specific exceptions represent business rule violations.

// domain/shared/repository/exceptions.ts
export class NotFound extends Error {
  constructor() {
    super("Item not found");
  }
}

export class AlreadyExists extends Error {
  constructor() {
    super("Item already exists");
  }
}

export class NotImplemented extends Error {
  constructor() {
    super("Method not implemented");
  }
}

Key points: - Clear, domain-specific error types - Used across all layers - Translated to HTTP responses in the Web Layer

Pagination

// domain/shared/repository/pagination.ts
export interface Paginated<T> {
  results: T[];
  count: number;
  page: number;
  pageSize: number;
}

export class Pagination {
  constructor(
    public page?: number,
    public pageSize?: number,
  ) {}

  static default(page: number = 1, pageSize: number = 20) {
    return { page, pageSize };
  }
}

Key points: - Standard pagination contract across all repositories - Default values provided - Results include total count for UI pagination

Domain Module Organization

domain/
├── accounts/           # Account entities, events, and repositories
│   ├── account.entity.ts
│   ├── account.repository.ts
│   ├── account_event.entity.ts
│   ├── account_event.repository.ts
│   ├── withdraw.entity.ts
│   ├── withdraw.repository.ts
│   ├── refund.entity.ts
│   └── refund.repository.ts
├── orders/             # Order entities and workflow
│   ├── order.entity.ts
│   ├── order.repository.ts
│   ├── chat.entity.ts
│   ├── chat.repository.ts
│   └── comment.repository.ts
├── payments/           # Payment abstractions
│   ├── payment.gateway.ts
│   └── payment_event.repository.ts
├── service/            # Service catalog
│   ├── service.entity.ts
│   ├── service.repository.ts
│   ├── category.entity.ts
│   └── category.repository.ts
├── transactions/       # Transaction history
│   └── transaction.repository.ts
├── user/               # User management
│   ├── user.entity.ts
│   ├── user.repository.ts
│   ├── role.entity.ts
│   ├── userrole.repository.ts
│   ├── permission.vo.ts
│   ├── identiy.entity.ts
│   ├── identity.repository.ts
│   ├── portfolio.entities.ts
│   └── portfolio.repository.ts
└── shared/             # Shared domain concepts
    └── repository/
        ├── base.ts
        ├── pagination.ts
        ├── transaction.ts
        └── exceptions.ts

Best Practices

✅ Do

  • Keep entities framework-agnostic – No NestJS decorators in domain entities
  • Encapsulate business rules – Logic belongs in entity methods, not use cases
  • Use value objects – For concepts without identity (permissions, statuses)
  • Define clear contracts – Repository interfaces define expectations
  • Use typed events – Type-safe event definitions prevent errors

❌ Don't

  • Don't import from outer layers – Domain never imports from app/, adapters/, or web/
  • Don't put infrastructure in domain – No SQL, HTTP, or file system code
  • Don't make anemic entities – Entities should have behavior, not just data
  • Don't leak implementation details – Repository interfaces hide database specifics

Summary

The Domain Layer is:

  • The source of truth for business logic
  • Framework-independent and testable in isolation
  • Stable – changes here are rare and significant
  • Abstract – defines contracts without implementation

By keeping the domain pure and focused, 2KRIKA achieves:

  • Easy testing – No mocking of databases or frameworks required
  • Flexibility – Swap out frameworks or databases without changing business logic
  • Clarity – Business rules are explicit and discoverable
  • Maintainability – Changes to infrastructure don't affect domain code