Skip to content

Technical Debt Register

Document Version: 1.0 Last Updated: 2026-01-17 Owner: Development Team


Overview

This document catalogs the identified technical debt in the Forma 3D Connect system, following the methodology from "Managing Technical Debt" (Kruchten, Nord, Ozkaya). Each debt item is classified by type, impact, and includes remediation recommendations.

Technical Debt Classification

Following the book's framework, we classify debt by:

Type Description
Architecture Debt Structural issues affecting system evolution
Code Debt Implementation issues reducing maintainability
Test Debt Missing or inadequate test coverage
Documentation Debt Missing or outdated documentation
Infrastructure Debt Build, deployment, and operational issues

Priority Levels

Priority Interest Rate Action Timeline
Critical High daily interest Immediate (sprint)
High Weekly interest accumulation Next 2-4 sprints
Medium Monthly interest Roadmap item
Low Minimal ongoing cost Opportunistic

Summary Dashboard

Category Critical High Medium Low Total Resolved
Architecture Debt 0 3 2 1 6 4
Code Debt 0 4 5 3 12 9
Test Debt 0 2 1 0 3 3
Documentation Debt 0 1 2 1 4 1
Infrastructure Debt 0 1 2 1 4 4
Total 0 11 12 6 29 21

Status: 21 of 29 items resolved (72%). See resolved items below marked with ✅.


Critical Priority Items

TD-001: In-Memory Webhook Idempotency Cache ✅ RESOLVED

Type: Architecture Debt Status:Resolved in Phase 5c Resolution Date: 2026-01-17

Resolution

Implemented database-backed webhook idempotency using the ProcessedWebhook table:

  • Storage: PostgreSQL table with unique constraint on webhookId
  • TTL: 24-hour expiry for idempotency records
  • Cleanup: Hourly scheduled job removes expired records
  • Race-condition safe: Uses database unique constraint for atomic check-and-mark

Files Changed:

  • prisma/schema.prisma - Added ProcessedWebhook model
  • apps/api/src/shopify/webhook-idempotency.repository.ts - New repository
  • apps/api/src/shopify/webhook-cleanup.service.ts - Cleanup scheduled job
  • apps/api/src/shopify/shopify.service.ts - Refactored to use repository
  • apps/api/src/shopify/shopify.module.ts - Updated providers

Original Problem

The Shopify service used an in-memory Set<string> for webhook idempotency tracking which caused:

  1. Horizontal Scaling Failure: Each API instance had its own cache
  2. Memory Leak: The Set grew unbounded
  3. Restart Data Loss: All idempotency data was lost on restart

TD-002: Missing Frontend Test Coverage ✅ RESOLVED

Type: Test Debt Status:Resolved in Phase 5d Resolution Date: 2026-01-17

Resolution

Implemented comprehensive frontend test coverage using Vitest with React Testing Library and MSW:

  • Test Framework: Vitest configured with jsdom environment
  • Mocking: MSW (Mock Service Worker) for API mocking
  • Coverage: 200 tests across hooks, contexts, API client, and UI components
  • Test Target: 60%+ coverage threshold configured

Files Created:

Category Files Test Count
Vitest Config apps/web/vitest.config.ts -
Test Setup apps/web/src/test/setup.ts -
Test Utilities apps/web/src/test/test-utils.tsx -
MSW Handlers apps/web/src/test/mocks/handlers.ts, server.ts -
Hook Tests apps/web/src/hooks/__tests__/*.test.tsx (6 files) 61
Context Tests apps/web/src/contexts/__tests__/*.test.tsx 17
API Client Tests apps/web/src/lib/__tests__/*.test.ts 37
UI Component Tests apps/web/src/components/ui/__tests__/*.test.tsx (5 files) 85
Total 15 test files 200 tests

Test Commands:

# Run all frontend tests
pnpm nx test web

# Run with coverage
pnpm nx test:coverage web

Original Problem

The React frontend had zero unit tests, relying entirely on E2E acceptance tests:

  1. Slow Feedback Loop: E2E tests take minutes; unit tests take seconds
  2. Fragile Test Suite: E2E tests were inherently flaky
  3. Component Regression Risk: UI logic changes could introduce subtle bugs
  4. Refactoring Fear: Developers avoided refactoring without test safety net

High Priority Items

TD-003: Untyped JSON Columns in Database Schema

Type: Code Debt Location: prisma/schema.prisma (multiple models) Interest Rate: Medium-High (type errors in production)

Description

Several database columns use Prisma's Json type without runtime validation:

model Order {
  shippingAddress    Json        // Untyped - could contain anything
}

model ProductMapping {
  defaultPrintProfile Json?      // Untyped print configuration
}

model AssemblyPart {
  printProfile Json?             // Untyped - duplicated pattern
}

Consequences

  1. Runtime Type Errors: Invalid data shapes cause crashes
  2. No IDE Support: Developers guess at JSON structure
  3. Inconsistent Validation: Each consumer validates differently
  4. Schema Drift: Frontend/backend expectations diverge

Remediation

  1. Define Zod schemas for all JSON columns
  2. Create typed interfaces in libs/domain
  3. Validate on read/write with transformers
// libs/domain/src/schemas/shipping-address.schema.ts
import { z } from 'zod';

export const ShippingAddressSchema = z.object({
  address1: z.string(),
  address2: z.string().optional(),
  city: z.string(),
  province: z.string().optional(),
  country: z.string(),
  zip: z.string(),
  phone: z.string().optional(),
});

export type ShippingAddress = z.infer<typeof ShippingAddressSchema>;

TD-004: Duplicated DTO Definitions Between Frontend and Backend

Type: Architecture Debt Location: apps/web/src/lib/api-client.ts and apps/api/src/**/dto/*.ts Interest Rate: Medium-High (sync issues cause bugs)

Description

The frontend manually defines response types that duplicate backend DTOs:

Frontend (api-client.ts):

export interface OrderResponse {
  id: string;
  shopifyOrderId: string;
  // ... 15+ more fields
}

Backend (order.dto.ts):

export class OrderDto {
  id!: string;
  shopifyOrderId!: string;
  // ... same 15+ fields
}

Consequences

  1. Type Drift: Frontend/backend types become inconsistent
  2. Double Maintenance: Every API change requires two updates
  3. No Compile-Time Safety: Mismatches only caught at runtime
  4. Nullable Confusion: Frontend may assume non-null incorrectly

Remediation Options

Option Effort Consistency Build Complexity
OpenAPI Generation 3-5 days High Medium
Shared Types Library 2-3 days Medium Low
GraphQL Codegen 5-7 days High High

Recommended: Extend libs/domain-contracts

// libs/domain-contracts/src/api/order.response.ts
export interface OrderApiResponse extends OrderDto {
  lineItems: LineItemApiResponse[];
  // Frontend-specific computed fields
  formattedPrice: string;
}

TD-005: Console Logging in Production Code

Type: Code Debt Location: Multiple files (65+ occurrences) Interest Rate: Medium

Description

Significant use of console.log/warn/error outside test utilities:

  • apps/api/src/observability/instrument.ts (2 occurrences)
  • apps/web/src/observability/sentry.ts (2 occurrences)
  • Many acceptance test files (expected)

While most console usage is in test setup code, the observability files should use structured logging.

Consequences

  1. Log Format Inconsistency: Console output bypasses Pino formatting
  2. Missing Context: No correlation IDs, timestamps, or metadata
  3. Hard to Filter: Unstructured logs difficult to query

Remediation

Replace console calls with injected logger:

// Before
console.log(`[Sentry] Initialized for environment: ${config.environment}`);

// After
this.logger.log({ 
  message: 'Sentry initialized',
  environment: config.environment 
});

TD-006: Missing Controller Tests

Type: Test Debt Location: apps/api/src/**/**.controller.ts Interest Rate: Medium-High

Description

Controller layer has minimal test coverage. Only fulfillment.controller.spec.ts exists for controllers:

Tested: - fulfillment.controller.spec.ts

Missing: - orders.controller.spec.ts - print-jobs.controller.spec.ts - product-mappings.controller.spec.ts - shopify.controller.spec.ts - simplyprint-webhook.controller.spec.ts - shipments.controller.spec.ts - sendcloud.controller.spec.ts - health.controller.spec.ts - event-log.controller.spec.ts - cancellation.controller.spec.ts

Consequences

  1. Request Validation Gaps: DTO validation not tested
  2. Guard Coverage Missing: API key guards not verified
  3. Error Response Untested: HTTP error formatting not validated
  4. Route Mapping Risk: Incorrect paths not caught

Remediation

Create controller tests using NestJS testing module:

describe('OrdersController', () => {
  let controller: OrdersController;
  let service: jest.Mocked<OrdersService>;

  beforeEach(async () => {
    const module = await Test.createTestingModule({
      controllers: [OrdersController],
      providers: [
        { provide: OrdersService, useValue: createMock<OrdersService>() },
      ],
    }).compile();

    controller = module.get(OrdersController);
    service = module.get(OrdersService);
  });

  describe('GET /orders', () => {
    it('should call service with pagination params', async () => {
      service.findAll.mockResolvedValue({ orders: [], total: 0 });

      await controller.findAll({ page: 1, pageSize: 10 });

      expect(service.findAll).toHaveBeenCalledWith({
        page: 1,
        pageSize: 10,
      });
    });
  });
});

TD-007: Incomplete Domain Contract Usage

Type: Architecture Debt Location: apps/api/src/orchestration/orchestration.service.ts:97-103 Interest Rate: Medium

Description

The Orchestration service mixes domain contracts with direct Prisma types, creating a leaky abstraction:

const jobs = await this.printJobsService.createPrintJobsForLineItem({
  id: lineItem.id,
  // ...
  status: lineItem.status as import('@prisma/client').LineItemStatus,
  // Type casting required - leaky abstraction
  unitPrice: 0 as unknown as import('@prisma/client/runtime/library').Decimal,
  // Awkward type coercion
});

Consequences

  1. Tight Prisma Coupling: Domain layer knows about database types
  2. Type Safety Loss: as unknown as casts bypass TypeScript
  3. Refactoring Difficulty: Database changes ripple through domain
  4. Testing Complexity: Mocks require Prisma type knowledge

Remediation

  1. Define all method signatures in domain contracts
  2. Convert at service boundaries only
  3. Use numeric types instead of Prisma Decimal in domain

TD-008: Missing Error Type Definitions

Type: Code Debt Location: Throughout backend services Interest Rate: Medium

Description

Error handling uses generic Error class or string messages rather than typed error hierarchies:

throw new Error(`Retry job not found: ${jobId}`);
// vs typed:
throw new RetryJobNotFoundError(jobId);

The codebase has SimplyPrintApiError as a good example, but this pattern isn't consistently applied.

Consequences

  1. Inconsistent Error Responses: Different formats for similar errors
  2. Difficult Error Handling: Consumers can't catch specific errors
  3. Missing Context: Stack traces without domain context
  4. Logging Gaps: Error metadata not captured

Remediation

Create error hierarchy in libs/domain:

// libs/domain/src/errors/base.error.ts
export abstract class DomainError extends Error {
  abstract readonly code: string;
  abstract readonly httpStatus: number;

  constructor(message: string, public readonly details?: Record<string, unknown>) {
    super(message);
    this.name = this.constructor.name;
  }
}

// libs/domain/src/errors/order.errors.ts
export class OrderNotFoundError extends DomainError {
  readonly code = 'ORDER_NOT_FOUND';
  readonly httpStatus = 404;

  constructor(orderId: string) {
    super(`Order not found: ${orderId}`, { orderId });
  }
}

TD-009: Hardcoded Configuration Values

Type: Infrastructure Debt Location: Multiple services Interest Rate: Medium

Description

Several configuration values are hardcoded rather than injected:

// retry-queue.service.ts
private readonly config: RetryConfig = {
  maxRetries: 5,
  initialDelayMs: 1000,
  maxDelayMs: 3600000,
  backoffMultiplier: 2,
};

// simplyprint-api.client.ts
timeout: 30000,

Consequences

  1. Environment Inflexibility: Can't tune per environment
  2. Testing Difficulty: Can't override for fast tests
  3. Operational Blindness: Values not visible in config dumps

Remediation

Move all configuration to environment variables with validation:

// env.validation.ts
@IsNumber()
@Min(1)
@Max(10)
RETRY_MAX_ATTEMPTS: number = 5;

@IsNumber()
RETRY_INITIAL_DELAY_MS: number = 1000;

Medium Priority Items

TD-010: ESLint Disable Comments in Test Setup ✅ RESOLVED

Type: Code Debt Status:Resolved in Phase 5l Resolution Date: 2026-01-17

Resolution

Created proper type declarations and configured ESLint overrides:

  • Created apps/api-e2e/src/support/global.d.ts for global variable declarations
  • Updated apps/api-e2e/eslint.config.mjs with test-specific rule overrides
  • Removed all eslint-disable comments from test setup files

TD-011: Unused nx-welcome Component ✅ RESOLVED

Type: Code Debt Status:Resolved in Phase 5m Resolution Date: 2026-01-17

Resolution

Removed unused Nx scaffolding:

  • Deleted apps/web/src/app/nx-welcome.tsx
  • Deleted apps/web/src/app/app.tsx
  • Deleted apps/web/src/app/app.module.css

The actual app entry point is at apps/web/src/app.tsx.


TD-012: Missing Rate Limiting ✅ RESOLVED

Type: Architecture Debt Status:Resolved in Phase 5n Resolution Date: 2026-01-17

Resolution

Implemented rate limiting with @nestjs/throttler:

  • Created apps/api/src/throttler/ module with configurable limits
  • Custom decorators: @WebhookThrottle(), @ReadThrottle(), @SkipThrottle()
  • Environment variables: RATE_LIMIT_TTL_SECONDS, RATE_LIMIT_DEFAULT, RATE_LIMIT_WEBHOOK
  • Health endpoints exempt, webhooks have lower limits

TD-013: Database Connection Pool Configuration ✅ RESOLVED

Type: Infrastructure Debt Status:Resolved in Phase 5o Resolution Date: 2026-01-17

Resolution

Added explicit Prisma pool configuration via environment variables:

  • DATABASE_POOL_SIZE (default: 10)
  • DATABASE_CONNECT_TIMEOUT_SECONDS (default: 5)
  • DATABASE_POOL_TIMEOUT_SECONDS (default: 10)

Pool parameters appended to DATABASE_URL as query parameters.


TD-014: Missing API Versioning Headers ✅ RESOLVED

Type: Architecture Debt Status:Resolved in Phase 5p Resolution Date: 2026-01-17

Resolution

Created versioning infrastructure in apps/api/src/versioning/:

  • ApiVersionInterceptor adds X-API-Version and X-API-Min-Version headers
  • @Deprecated() decorator for marking endpoints with sunset dates
  • DeprecatedInterceptor adds deprecation headers (Sunset, X-API-Deprecated)
  • Both interceptors registered globally

TD-015: Incomplete Shipment Service Tests ✅ RESOLVED

Type: Test Debt Status:Resolved in Phase 5q Resolution Date: 2026-01-17

Resolution

Added comprehensive unit tests:

  • apps/api/src/shipments/__tests__/shipments.service.spec.ts (14 tests)
  • apps/api/src/shipments/__tests__/shipments.controller.spec.ts (8 tests)
  • Total: 22 new tests added (414 → 436)

TD-016: Generic Record Types for Metadata ✅ RESOLVED

Type: Code Debt Status:Resolved in Phase 5r Resolution Date: 2026-01-17

Resolution

Created typed Zod schemas for metadata:

  • libs/domain/src/schemas/order-metadata.schema.ts - OrderMetadata type
  • libs/domain/src/schemas/print-job-metadata.schema.ts - PrintJobMetadata type
  • Both include parse/safeParse functions for runtime validation

TD-017: Direct Prisma Import in Domain Contracts ✅ RESOLVED

Type: Architecture Debt Status:Already resolved in Phase 5b Resolution Date: 2026-01-17

Resolution

Domain contracts already use string union types instead of Prisma imports:

export type OrderStatusType = 'PENDING' | 'PROCESSING' | ...;

No @prisma/client imports found in libs/ folder.


Low Priority Items

TD-018: Unused Imports and Dead Code ✅ RESOLVED

Type: Code Debt Status:Resolved in Phase 5t Resolution Date: 2026-01-17

Resolution

Ran ESLint auto-fix and removed unnecessary React fragment in apps/web/src/router.tsx.


TD-019: Inconsistent File Naming ✅ RESOLVED

Type: Code Debt Status:Resolved in Phase 5u Resolution Date: 2026-01-17

Resolution

Renamed non-kebab-case files:

  • ErrorBoundary.tsxerror-boundary.tsx
  • Only one file found not following kebab-case convention

TD-020: Missing OpenAPI Response Examples ✅ RESOLVED

Type: Documentation Debt Status:Already present Resolution Date: 2026-01-17

Resolution

Verified that DTOs already have comprehensive @ApiProperty examples. No action needed.


TD-021: Test Data Fixtures Duplication ✅ RESOLVED

Type: Test Debt Status:Resolved in Phase 5w Resolution Date: 2026-01-17

Resolution

Created centralized testing library @forma3d/testing:

  • libs/testing/src/fixtures/order.fixtures.ts
  • libs/testing/src/fixtures/shipment.fixtures.ts
  • libs/testing/src/fixtures/print-job.fixtures.ts

Factory functions: createMockOrder(), createMockShipment(), createMockPrintJob(), etc.


Debt Remediation Roadmap

Phase 1: Critical Items ✅ COMPLETE

  1. TD-001: Implement database-backed webhook idempotency ✅ DONE (Phase 5c)
  2. TD-002: Set up Vitest for frontend, add hook tests ✅ DONE (Phase 5d)

Phase 2: High Priority Items (Phases 5e-5k) ✅ COMPLETE

Phase Tech Debt Description Effort Status
5e TD-003 Add Zod schemas for JSON columns 0.5 week ✅ DONE
5f TD-004 Shared API types (eliminate duplicates) 0.5 week ✅ DONE
5g TD-005 Structured logging (replace console.log) 0.5 week ✅ DONE
5h TD-006 Add missing controller tests 0.5 week ✅ DONE
5i TD-007 Clean up domain contract usage 0.5 week ✅ DONE
5j TD-008 Create typed error hierarchy 0.5 week ✅ DONE
5k TD-009 Externalize configuration 0.5 week ✅ DONE

AI Implementation Time: ~2.5 hours (vs 3.5 weeks estimated for human team)

Phase 3: Medium/Low Priority Items (Phases 5l-5w) ✅ COMPLETE

Phase Tech Debt Description Status
5l TD-010 ESLint disable comments cleanup ✅ DONE
5m TD-011 Unused nx-welcome component ✅ DONE
5n TD-012 Rate limiting implementation ✅ DONE
5o TD-013 Database connection pool config ✅ DONE
5p TD-014 API versioning headers ✅ DONE
5q TD-015 Shipment service tests ✅ DONE
5r TD-016 Metadata type definitions ✅ DONE
5s TD-017 Prisma decoupling (already done) ✅ DONE
5t TD-018 Dead code cleanup ✅ DONE
5u TD-019 File naming standardization ✅ DONE
5v TD-020 OpenAPI examples (already present) ✅ DONE
5w TD-021 Test fixtures centralization ✅ DONE

AI Implementation Time: ~1.5 hours (vs 2 weeks estimated for human team)

Implementation Feedback: See docs/_internal/prompts/techdebt-medium-low-implementation-feedback.md

Summary

Category Items Resolved Remaining
Critical Priority 2 2 0
High Priority 9 9 0
Medium Priority 8 8 0
Low Priority 4 4 0
Total 23 21 2

Note: 2 items (TD-017, TD-020) were already resolved in previous phases.


Metrics and Tracking

Key Indicators

Metric Current Target Tracking
Frontend Test Coverage 60%+ 60% Vitest coverage ✅
Backend Test Coverage ~50% 80% Jest coverage
Critical Debt Items 0 0 This register ✅
High Priority Items 11 <5 This register
ESLint Violations 1 0 CI/CD

Review Schedule

  • Weekly: Review critical items in standup
  • Sprint Planning: Allocate capacity for high priority items
  • Quarterly: Full register review and prioritization

References

  • Kruchten, P., Nord, R., & Ozkaya, I. (2019). Managing Technical Debt: Reducing Friction in Software Development
  • Project Rules: .cursorrules (forbid any, eslint-disable, etc.)
  • Architecture Decisions: docs/03-architecture/adr/