Skip to content

Current Architecture Analysis

The project is a Purchase Request System (PRS) built with Node.js, Fastify, PostgreSQL, and Sequelize. It follows a layered architecture with some elements of Domain-Driven Design, but there are several areas where it deviates from DDD best practices.

Current Structure

Layers

The codebase is organized into the following layers:

  1. Domain Layer (src/domain/):
  2. Contains entities and constants
  3. Primarily consists of validation schemas using Zod
  4. Lacks behavior and domain logic

  5. Application Layer (src/app/):

  6. Contains services, controllers, and utilities
  7. Most business logic resides here
  8. Controllers handle HTTP requests and responses
  9. Services orchestrate business operations

  10. Infrastructure Layer (src/infra/):

  11. Contains repositories, database models, and logs
  12. Implements data access and persistence
  13. Manages external resources and integrations

  14. Interface Layer (src/interfaces/):

  15. Contains routes and API endpoints
  16. Defines the API contract
  17. Handles routing and middleware

Dependency Injection

  • Uses Awilix for dependency injection
  • Container is defined in src/container.js
  • Components are registered and resolved at runtime
  • Promotes loose coupling between components

Repository Pattern

  • Uses a BaseRepository class that all repositories extend
  • Repositories handle database operations and are in the infrastructure layer
  • Provides common CRUD operations
  • Specific repositories extend the base with domain-specific queries

Entity Validation

  • Uses Zod for schema validation
  • Entity schemas are defined in the domain layer
  • Provides type safety and validation
  • Ensures data integrity

DDD Issues in Current Implementation

1. Anemic Domain Model

The domain model is anemic, meaning it lacks behavior and business logic:

  • Domain entities are primarily data structures with validation schemas
  • Business logic is mostly in services rather than in domain entities
  • Entities lack behavior and methods
  • Domain objects don't enforce invariants

Example from the codebase:

JavaScript
// Domain entity is just a validation schema
const createRequisitionSchema = z
  .object({
    isDraft: z
      .enum(['true'], {
        required_error: 'Draft status is required',
        invalid_type_error: 'Invalid draft status',
      })
      .transform((value) => value === 'true'),
    type: z.enum(['ofm', 'non-ofm', 'ofm-tom', 'non-ofm-tom', 'transfer'], {
      message: REQUIRED_FIELD_ERROR,
    }),
    // ...more validation rules
  });

2. Infrastructure Leakage

The domain layer has knowledge of infrastructure concerns:

  • Domain entities are tightly coupled with Sequelize models
  • Domain layer has knowledge of infrastructure concerns
  • No clear separation between domain and persistence models
  • Database concerns leak into the domain layer

3. Missing Aggregates and Bounded Contexts

The codebase lacks clear aggregate roots and bounded contexts:

  • No clear aggregate roots or bounded contexts
  • Relationships between entities are defined at the database level, not the domain level
  • No explicit transaction boundaries
  • No clear ownership of related entities

4. Missing Value Objects

The codebase uses primitive types instead of value objects:

  • Primitive types are used instead of value objects for domain concepts
  • No encapsulation of domain concepts like Money, Email, Address, etc.
  • No validation or behavior for value types
  • Loss of domain semantics

5. Missing Domain Events

There's no evidence of domain events for communication between bounded contexts:

  • No domain events for communication between bounded contexts
  • No event-driven architecture
  • No clear way to handle cross-domain concerns
  • Tight coupling between domains

6. Business Logic in Services

Business rules are implemented in services rather than in the domain model:

  • Business rules are scattered across services
  • Domain logic is not encapsulated in domain objects
  • Services contain both orchestration and domain logic
  • Difficult to enforce invariants

Example from the codebase:

JavaScript
// Business logic in service instead of domain entity
async createRequisition(request, reply) {
  // ... validation and setup

  try {
    requisition = await this.requisitionRepository.create(body, {
      transaction,
    });

    const rsApprovers = await this.requisitionService.rsApproversV2({
      category,
      projectId,
      departmentId,
      userFromToken,
      transaction,
      companyId,
    });

    await this.requisitionService.assignRSApprovers({
      rsApprovers,
      category,
      requisitionId: requisition.id,
      transaction,
    });

    await transaction.commit();
    // ... more logic
  } catch (error) {
    await transaction.rollback();
    throw this.clientErrors.BAD_REQUEST({
      message: 'Requisition creation failed',
    });
  }
}