Carbonteq
Best Practices/Backend/Architecture/Domain

Best Practices

Import Reference Patterns

Don't

Use relative imports for cross-folder navigation. This creates:

  • Brittle dependencies with deep ../../../ paths
  • Difficult refactoring when moving files
  • Obscured module boundaries
  • Inconsistent import patterns
DoClick to view

Use TypeScript path aliases for cross-folder imports. They provide:

  • Clear module boundaries with consistent import paths
  • Easy refactoring when restructuring code
  • Clean navigation without deep relative paths
  • Consistent patterns across the codebase
// Current file: domain/entities/user/host.entity.ts
// Bad: Deep relative imports for cross-folder access
import type {IEntity} from "../../../utils/base.entity";
import type {UUID} from "../../../utils/refined.types";
import {BaseEntity} from "../../../utils/base.entity";
// Bad: Mixing relative and path alias patterns
import type {IEntity} from "@domain/utils/base.entity"; // Path alias
import {BaseEntity} from "../../../utils/base.entity"; // Relative import for same target
// Bad: Relative imports for different folders
import {UserRepository} from "../user/user.repository"; // Should use @domain/entities/user/
import {JamEntity} from "../../jam/jam.entity"; // Should use @domain/entities/jam/

Structured Class Method Ordering

Don't

Place methods in random order without structure. This causes:

  • Difficult code navigation
  • Reduced code readability
  • Inconsistent patterns across entities
  • Mixed concerns making class structure unclear
DoClick to view

Organize methods by functional category. This provides:

  • Predictable code organization with consistent structure
  • Improved maintainability through logical grouping
  • Easy navigation with methods in expected places
  • Clear separation of concerns
export class Host extends BaseEntity implements IEntity {
readonly userId: UUID;
// Bad: Mixed ordering
updateProfile(updates: SerializedHostUpdate): E.Effect<Host, HostError, never> {
// Domain method mixed with constructor
}
private constructor(data: Readonly<HostType>) {
// Constructor in middle
}
get displayName(): string {
// Getter after domain method
}
static create(input: SerializedHost): E.Effect<Host, HostError, never> {
// Factory method at bottom
}
serialized(): E.Effect<SerializedHost, ParseResult.ParseError, never> {
// Serialization mixed in
}
}

Schema-Derived Type Definitions

Don't

Define types separately from schemas. This leads to:

  • Duplicated type definitions
  • Inconsistencies between schema and types
  • Maintenance issues when schema changes
  • Type mismatches with complex nested relationships
DoClick to view

Derive types from schemas with proper nested entity handling. This provides:

  • Single source of truth for type definitions
  • Automatic consistency between schema and types
  • Proper type boundaries for nested entities
  • Type safety throughout the application
// Bad: Separate type definition
export interface JamType {
id: UUID;
title: string;
rounds: Round[]; // Duplicated from schema
}
// Bad: Direct encoded type usage with entity confusion
export type SerializedJam = S.Schema.Encoded<typeof JamSchema>; // Mixes entity and serialized types
// Bad: Manual type definition
export interface SerializedJam {
id: string;
title: string;
rounds: any[]; // Loses type safety
}

Entity Mutation Patterns

Don't

Mutate entity properties directly. This approach:

  • Breaks immutability principles
  • Bypasses validation logic
  • Causes inconsistent state
  • Makes tracking changes and debugging difficult
DoClick to view

Return new entity instances with serialization. This ensures:

  • Data integrity through immutable updates
  • Proper validation on every change
  • Consistent state throughout the application
  • Easy debugging with clear change tracking
// Bad: Direct mutation
updateProfile(updates: SerializedHostUpdate): void {
this.firstName = updates.firstName;
this.lastName = updates.lastName;
this.updatedAt = new Date(); // Direct assignment
}
// Bad: Returning mutated this
updateProfile(updates: SerializedHostUpdate): Host {
Object.assign(this, updates);
return this; // Returns same instance
}
// Bad: No validation on updates
updateProfile(updates: any): Host {
return new Host({...this, ...updates}); // Bypasses create() validation
}

Property Declaration Patterns

Don't

Use mutable properties with nullable types. This allows:

  • Uncontrolled state changes bypassing validation
  • Runtime null reference errors
  • Hidden optionality that's not explicit
  • Mixed mutability patterns creating confusion
DoClick to view

Use readonly properties with Option types. They provide:

  • Compile-time immutability enforcement
  • Explicit optionality with Option types
  • Prevention of null reference errors
  • Clear intent about property mutability
// Bad: Mutable properties
export class Host extends BaseEntity implements IHost {
userId: UUID; // Can be reassigned
dob?: Date; // Hidden optionality
phoneNumber: number | null; // Nullable instead of Option
address: Address; // Mutable reference
// Bad: Allows direct mutation
setPhoneNumber(phone: number): void {
this.phoneNumber = phone; // Direct assignment
}
}
// Bad: Mixed mutability
export class Host extends BaseEntity implements IHost {
readonly userId: UUID; // Good
dob: Date | undefined; // Bad: mutable and nullable
phoneNumber?: number; // Bad: hidden optionality
}

Schema and Entity Optional Property Patterns

Don't

Use inconsistent optional type handling. This creates:

  • Type safety issues with missing Optional wrapper
  • Runtime errors from nullable types
  • Confusion with mixed optional patterns
  • Validation inconsistencies
DoClick to view

Use consistent Optional types across schema and entity. This ensures:

  • Type-safe optionality throughout the system
  • Consistent validation between schema and runtime
  • Clear intent about optional vs required fields
  • Runtime safety with proper null handling
// Bad: Missing Optional wrapper in schema
export const HostSchema = S.Struct({
dob: S.DateFromSelf, // Required in schema
phoneNumber: S.Number, // Required in schema
});
// Bad: Nullable types instead of Option
export class Host extends BaseEntity implements IHost {
readonly dob?: Date; // TypeScript optional
readonly phoneNumber: number | null; // Nullable type
readonly address: Address | undefined; // Undefined type
}
// Bad: Mixed patterns
export class Host extends BaseEntity implements IHost {
readonly dob: O.Option<Date>; // Good
readonly phoneNumber?: number; // Bad: inconsistent with schema
}

Value Object Instantiation Patterns

Don't

Create complex objects inside constructor or after entity creation. This approach:

  • Breaks validation flow and Effect chain
  • Mixes validation concerns with construction logic
  • Leads to partially valid entities
  • Creates inconsistent error handling
DoClick to view

Create complex value objects with external validation before constructor. This ensures:

  • Proper Effect chain with all validations
  • Separated validation concerns from construction
  • Complete validation before entity creation
  • Consistent error handling throughout the system
// Bad: Complex object creation inside constructor
private constructor(data: SerializedHost) {
super();
// Validation happens inside constructor - breaks Effect chain
this.address = new Address(data.address); // No validation
this.socialLinks = new SocialLinks(data.socialLinks); // No validation
}
// Bad: Separate validation steps
static create(input: SerializedHost): E.Effect<Host, HostError, never> {
return S.decodeUnknown(HostSchema)(input).pipe(
E.map((data) => {
const host = new Host(data); // Entity created before all validation
// Validation happens after entity creation
host.validateAddress(); // Separate validation step
host.validateSocialLinks(); // Not in Effect chain
return host;
})
);
}
// Bad: Manual object creation without validation
static create(input: SerializedHost): Host {
const address = { ...input.address }; // No Address.create()
const socialLinks = { ...input.socialLinks }; // No SocialLinks.create()
return new Host(input, address, socialLinks); // No validation at all
}

Error Class Definition Patterns

Don't

Use generic errors without context or structure. This provides:

  • No domain context for debugging
  • Inconsistent error handling patterns
  • Missing structured information
  • Difficult error case handling
DoClick to view

Create domain-specific errors with structured context. They provide:

  • Clear domain context for specific errors
  • Structured parameters with field, value, and details
  • Consistent message formatting across the domain
  • Proper inheritance hierarchy for error categorization
// Bad: Generic Error with no domain context
export class HostError extends Error {
constructor(message: string) {
super(message); // No structured information
}
}
// Bad: String literal errors
throw new Error("Host validation failed"); // No context about what failed
// Bad: Inconsistent error messages
throw new Error("Invalid email"); // Different format
throw new Error("Host phone number bad"); // Inconsistent naming
throw new Error("ERROR: dob wrong"); // No standardization
// Bad: No inheritance hierarchy
export class HostValidationError extends Error { // Should extend ValidationError
constructor(message: string) {
super(message); // Loses base error categorization
}
}
// Bad: No structured parameters
export class HostValidationError extends ValidationError {
constructor(message: string) { // No field/value context
super(message);
}
}