Skip to main content

Module Design

Module Design Document

Project: {{PROJECT_NAME}} Module: {{MODULE_NAME}} Service: {{SERVICE_NAME}} Version: {{VERSION}} Date: {{DATE}} Author: {{AUTHOR}} Status: Draft | In Review | Approved Reviewers: {{REVIEWERS}}

Document History

Version Date Author Changes
0.1 {{DATE}} {{AUTHOR}} Initial draft

1. Module Overview & Responsibility

Module: {{MODULE_NAME}} Layer: Domain | Application | Infrastructure | Presentation Repository: {{REPO_OR_MONOREPO_PATH}} Team Owner: {{TEAM_NAME}}

Single Responsibility Statement:

{{THE_MODULE_IS_RESPONSIBLE_FOR_ONE_THING}}

This module owns:

  • {{OWNED_RESOURCE_1}} (data + business logic)
  • {{OWNED_RESOURCE_2}}

This module does NOT own:

  • {{NOT_OWNED_1}} — owned by {{OTHER_MODULE}}
  • {{NOT_OWNED_2}} — owned by {{OTHER_MODULE}}

Why this is a separate module: {{RATIONALE_FOR_SEPARATION}} — e.g., "Separate bounded context, different team ownership, different scaling requirements"


2. Interface Definition (Public API)

2.1 Exported Service Interface

// Public interface exported by this module
export interface I{{ModuleName}}Service {
  /**
   * {{METHOD_1_DESCRIPTION}}
   * @throws {ValidationError} if dto is invalid
   * @throws {ConflictError} if {{UNIQUE_FIELD}} already exists
   */
  create(dto: Create{{Entity}}Dto, context: RequestContext): Promise<{{Entity}}>;

  /**
   * {{METHOD_2_DESCRIPTION}}
   * @throws {NotFoundError} if not found
   */
  findById(id: string, context: RequestContext): Promise<{{Entity}}>;

  /**
   * {{METHOD_3_DESCRIPTION}}
   */
  findAll(filter: {{Filter}}Dto, context: RequestContext): Promise<PaginatedResult<{{Entity}}>>;

  /**
   * {{METHOD_4_DESCRIPTION}}
   * @throws {NotFoundError} if not found
   * @throws {ForbiddenError} if user lacks permission
   */
  update(id: string, dto: Update{{Entity}}Dto, context: RequestContext): Promise<{{Entity}}>;

  /**
   * {{METHOD_5_DESCRIPTION}}
   * @throws {NotFoundError} if not found
   */
  delete(id: string, context: RequestContext): Promise<void>;
}

// DTOs exported for consumers
export type Create{{Entity}}Dto = { /* ... */ };
export type Update{{Entity}}Dto = { /* ... */ };
export type {{Filter}}Dto = { /* ... */ };
export type {{Entity}} = { /* ... */ };

2.2 HTTP Endpoints (if applicable)

Method Path Auth Description
POST /api/v{{V}}/{{resource}} JWT Create {{entity}}
GET /api/v{{V}}/{{resource}} JWT List {{entities}}
GET /api/v{{V}}/{{resource}}/:id JWT Get by ID
PUT /api/v{{V}}/{{resource}}/:id JWT Full update
PATCH /api/v{{V}}/{{resource}}/:id JWT Partial update
DELETE /api/v{{V}}/{{resource}}/:id JWT Soft delete

2.3 Events Published

Event Topic/Queue Schema Triggered By
{{entity}}.created {{TOPIC}} See §5 POST endpoint
{{entity}}.updated {{TOPIC}} See §5 PUT/PATCH endpoint
{{entity}}.deleted {{TOPIC}} See §5 DELETE endpoint
{{entity}}.{{CUSTOM_EVENT}} {{TOPIC}} See §5 {{BUSINESS_TRIGGER}}

3. Internal Structure

{{MODULE_NAME}}/
├── controllers/
│   └── {{entity}}.controller.ts      # HTTP request handling, input parsing
├── services/
│   └── {{entity}}.service.ts         # Business logic
├── repositories/
│   ├── {{entity}}.repository.ts      # Data access interface
│   └── {{entity}}.repository.pg.ts   # PostgreSQL implementation
├── domain/
│   ├── {{entity}}.entity.ts          # Domain entity / value objects
│   ├── {{entity}}.events.ts          # Domain events
│   └── {{entity}}.errors.ts          # Domain-specific errors
├── dto/
│   ├── create-{{entity}}.dto.ts
│   ├── update-{{entity}}.dto.ts
│   └── {{entity}}-filter.dto.ts
├── mappers/
│   └── {{entity}}.mapper.ts          # DB record ↔ domain entity ↔ DTO
├── __tests__/
│   ├── unit/
│   └── integration/
└── {{entity}}.module.ts              # Module registration / DI wiring

Layer rules (enforced by linting):

  • Controllers only call Services (never Repositories directly)
  • Services only call Repositories and publish Events
  • Domain entities have no framework dependencies
  • Mappers live at service layer — not in controllers

4. Database Schema

Primary Table: {{table_name}}

Column Type Nullable Default Constraints Description
id UUID NO gen_random_uuid() PK Surrogate key
created_at TIMESTAMPTZ NO NOW() Immutable creation time
updated_at TIMESTAMPTZ NO NOW() Auto-updated on write
deleted_at TIMESTAMPTZ YES NULL Soft delete marker
version INTEGER NO 1 Optimistic lock version
{{FIELD_1}} {{TYPE}} {{YES/NO}} {{DEFAULT}} {{CHECK/UNIQUE/FK}} {{DESCRIPTION}}
{{FIELD_2}} {{TYPE}} {{YES/NO}} {{DEFAULT}} {{CHECK/UNIQUE/FK}} {{DESCRIPTION}}
{{FK_ID}} UUID NO FK → {{other_table}}(id) {{RELATIONSHIP}}

Indexes:

CREATE INDEX CONCURRENTLY idx_{{table_name}}_{{column}} ON {{table_name}}({{column}})
    WHERE deleted_at IS NULL;
-- Rationale: {{WHY_THIS_INDEX}}

CREATE UNIQUE INDEX idx_{{table_name}}_{{unique_column}} ON {{table_name}}({{unique_column}})
    WHERE deleted_at IS NULL;
-- Rationale: Enforce uniqueness for active records only

RLS (Row-Level Security):

-- TODO: Enable if multi-tenant
-- ALTER TABLE {{table_name}} ENABLE ROW LEVEL SECURITY;
-- CREATE POLICY {{table_name}}_tenant_isolation ON {{table_name}}
--     USING (tenant_id = current_setting('app.current_tenant_id')::uuid);

5. API Endpoints (Detailed)

POST /api/v{{V}}/{{resource}}

Request:

{
  "{{field1}}": "string (required, max 255 chars)",
  "{{field2}}": "number (required, > 0)",
  "{{field3}}": "string (optional, enum: [A, B, C])"
}

Success 201:

{
  "id": "uuid",
  "{{field1}}": "value",
  "{{field2}}": 0,
  "createdAt": "ISO8601"
}

Event published: {{entity}}.created

{
  "specversion": "1.0",
  "type": "{{entity}}.created",
  "source": "/{{resource}}",
  "id": "{{EVENT_UUID}}",
  "time": "ISO8601",
  "data": {
    "entityId": "uuid",
    "tenantId": "uuid",
    "createdBy": "uuid",
    "{{field1}}": "value"
  }
}

6. Business Logic Specifications

6.1 Business Rules

Rule ID Rule Enforced In Error
BR-001 {{RULE_1_DESCRIPTION}} Service {{ERROR_CODE}}
BR-002 {{RULE_2_DESCRIPTION}} Domain Entity {{ERROR_CODE}}
BR-003 {{RULE_3_DESCRIPTION}} Service + DB constraint ConflictError

6.2 Validation Rules

Field Type Required Validation Error Message
{{field1}} string Yes Min 1, Max 255 chars "{{field1}} must be 1-255 characters"
{{field2}} number Yes Positive integer "{{field2}} must be a positive integer"
{{field3}} enum No One of: [A, B, C] "{{field3}} must be A, B, or C"

6.3 Authorization Rules

Operation Required Role Additional Conditions
Create {{ROLE}}
Read own Any authenticated user userId === resource.createdBy
Read any {{ADMIN_ROLE}}
Update {{ROLE}} userId === resource.createdBy OR isAdmin
Delete {{ADMIN_ROLE}} Soft delete only
Hard delete SUPER_ADMIN Requires 2FA confirmation

7. Event Publishing / Consuming

7.1 Events Published

Event When Payload Schema Idempotency Key
{{entity}}.created After successful create {entityId, tenantId, ...} entityId
{{entity}}.updated After successful update {entityId, changes: {...}} entityId + version
{{entity}}.deleted After soft delete {entityId, deletedAt} entityId

7.2 Events Consumed

Event Source Module Handler Processing Guarantee
{{other_entity}}.deleted {{OTHER_MODULE}} Cascade soft-delete related records At-least-once
{{other_entity}}.updated {{OTHER_MODULE}} Update denormalized cache At-least-once

Idempotency strategy: All consumers check processed_events table before processing. Duplicate events are logged and skipped.


8. Dependencies

8.1 Upstream (what this module depends on)

Dependency Type Coupling Reason
AuthModule Internal module Loose (interface) JWT validation, user context
{{OTHER_MODULE}} Internal module Loose (events) {{REASON}}
PostgreSQL Infrastructure Required Primary storage
Redis Infrastructure Optional Caching (degrades gracefully)
{{EXTERNAL_SDK}} External library Hard {{REASON}}

8.2 Downstream (what depends on this module)

Consumer What they use Notes
{{OTHER_MODULE}} {{entity}}.created events Read-only consumer
{{FRONTEND}} HTTP API Via API gateway

9. Error Handling & Recovery

Error Scenario Handling User Impact Recovery
DB connection lost Retry 3x with backoff, then 503 Request fails gracefully Auto-recover when DB reconnects
External API timeout Return cached data or 503 Degraded feature Async retry, alert on-call
Duplicate submission Detect via unique constraint, return 409 Clear error message None needed
Invalid state transition Return 422 with state machine error Clear error message User corrects input
Event publish failure Log to retry queue, return 202 Async delay Background retry

10. Configuration & Feature Flags

Environment Variables

Variable Type Default Description
{{MODULE_NAME}}_CACHE_TTL number 300 Cache TTL seconds
{{MODULE_NAME}}_MAX_LIST_SIZE number 100 Max items per page
{{MODULE_NAME}}_RATE_LIMIT_RPM number 60 Rate limit per minute

Feature Flags

Flag Type Default Description
{{MODULE_NAME}}_ENABLE_CACHE boolean true Toggle Redis caching
{{MODULE_NAME}}_ENABLE_{{FEATURE}} boolean false Gradual rollout of {{FEATURE}}

11. Monitoring & Health Checks

Health Check Endpoint

GET /health/{{module-name}}

{
  "status": "healthy | degraded | unhealthy",
  "checks": {
    "database": "healthy",
    "cache": "healthy | degraded",
    "externalApi": "healthy | degraded | unhealthy"
  },
  "latency": {
    "database_ms": 5,
    "cache_ms": 1
  }
}

Key Metrics

Metric Type Alert Threshold Dashboard
{{module}}_requests_total Counter {{DASHBOARD_LINK}}
{{module}}_request_duration_ms Histogram p99 > {{THRESHOLD}}ms {{DASHBOARD_LINK}}
{{module}}_errors_total Counter Error rate > {{THRESHOLD}}% {{DASHBOARD_LINK}}
{{module}}_cache_hit_rate Gauge < {{THRESHOLD}}% for 5min {{DASHBOARD_LINK}}
{{module}}_db_pool_exhausted Counter Any occurrence {{DASHBOARD_LINK}}

12. Primary Flow — Sequence Diagram

sequenceDiagram
    autonumber
    participant C as Controller
    participant S as Service
    participant V as Validator
    participant R as Repository
    participant DB as PostgreSQL
    participant EB as Event Bus
    participant Cache as Redis

    C->>V: validate(dto)
    alt Invalid input
        V-->>C: ValidationError
        C-->>Client: 400 Bad Request
    end
    V-->>C: Validated DTO

    C->>S: create(dto, context)
    S->>S: checkBusinessRules(dto)
    alt Business rule violation
        S-->>C: BusinessRuleError
        C-->>Client: 422 Unprocessable
    end

    S->>DB: BEGIN TRANSACTION
    S->>R: create(entityData)
    R->>DB: INSERT INTO {{table_name}}
    DB-->>R: Inserted record
    R-->>S: {{Entity}} domain object

    S->>DB: COMMIT

    S->>EB: publish("{{entity}}.created", event)
    Note over EB: Async — does not block response

    S->>Cache: INVALIDATE related keys
    S-->>C: {{Entity}} DTO
    C-->>Client: 201 Created

Approval

Role Name Date Signature
Author
Module Owner
Tech Lead
Reviewer