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- AddedProcessedWebhookmodelapps/api/src/shopify/webhook-idempotency.repository.ts- New repositoryapps/api/src/shopify/webhook-cleanup.service.ts- Cleanup scheduled jobapps/api/src/shopify/shopify.service.ts- Refactored to use repositoryapps/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:
- Horizontal Scaling Failure: Each API instance had its own cache
- Memory Leak: The Set grew unbounded
- 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:
- Slow Feedback Loop: E2E tests take minutes; unit tests take seconds
- Fragile Test Suite: E2E tests were inherently flaky
- Component Regression Risk: UI logic changes could introduce subtle bugs
- 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¶
- Runtime Type Errors: Invalid data shapes cause crashes
- No IDE Support: Developers guess at JSON structure
- Inconsistent Validation: Each consumer validates differently
- Schema Drift: Frontend/backend expectations diverge
Remediation¶
- Define Zod schemas for all JSON columns
- Create typed interfaces in
libs/domain - 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¶
- Type Drift: Frontend/backend types become inconsistent
- Double Maintenance: Every API change requires two updates
- No Compile-Time Safety: Mismatches only caught at runtime
- 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¶
- Log Format Inconsistency: Console output bypasses Pino formatting
- Missing Context: No correlation IDs, timestamps, or metadata
- 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¶
- Request Validation Gaps: DTO validation not tested
- Guard Coverage Missing: API key guards not verified
- Error Response Untested: HTTP error formatting not validated
- 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¶
- Tight Prisma Coupling: Domain layer knows about database types
- Type Safety Loss:
as unknown ascasts bypass TypeScript - Refactoring Difficulty: Database changes ripple through domain
- Testing Complexity: Mocks require Prisma type knowledge
Remediation¶
- Define all method signatures in domain contracts
- Convert at service boundaries only
- 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¶
- Inconsistent Error Responses: Different formats for similar errors
- Difficult Error Handling: Consumers can't catch specific errors
- Missing Context: Stack traces without domain context
- 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¶
- Environment Inflexibility: Can't tune per environment
- Testing Difficulty: Can't override for fast tests
- 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.tsfor global variable declarations - Updated
apps/api-e2e/eslint.config.mjswith test-specific rule overrides - Removed all
eslint-disablecomments 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/:
ApiVersionInterceptoraddsX-API-VersionandX-API-Min-Versionheaders@Deprecated()decorator for marking endpoints with sunset datesDeprecatedInterceptoradds 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 typelibs/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.tsx→error-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.tslibs/testing/src/fixtures/shipment.fixtures.tslibs/testing/src/fixtures/print-job.fixtures.ts
Factory functions: createMockOrder(), createMockShipment(), createMockPrintJob(), etc.
Debt Remediation Roadmap¶
Phase 1: Critical Items ✅ COMPLETE¶
TD-001: Implement database-backed webhook idempotency✅ DONE (Phase 5c)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(forbidany,eslint-disable, etc.) - Architecture Decisions:
docs/03-architecture/adr/