Best Practices
Import Reference Patterns
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
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 accessimport 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 patternsimport type {IEntity} from "@domain/utils/base.entity"; // Path aliasimport {BaseEntity} from "../../../utils/base.entity"; // Relative import for same target// Bad: Relative imports for different foldersimport {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
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
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 orderingupdateProfile(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
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
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 definitionexport interface JamType {id: UUID;title: string;rounds: Round[]; // Duplicated from schema}// Bad: Direct encoded type usage with entity confusionexport type SerializedJam = S.Schema.Encoded<typeof JamSchema>; // Mixes entity and serialized types// Bad: Manual type definitionexport interface SerializedJam {id: string;title: string;rounds: any[]; // Loses type safety}
Entity Mutation Patterns
Mutate entity properties directly. This approach:
- Breaks immutability principles
- Bypasses validation logic
- Causes inconsistent state
- Makes tracking changes and debugging difficult
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 mutationupdateProfile(updates: SerializedHostUpdate): void {this.firstName = updates.firstName;this.lastName = updates.lastName;this.updatedAt = new Date(); // Direct assignment}// Bad: Returning mutated thisupdateProfile(updates: SerializedHostUpdate): Host {Object.assign(this, updates);return this; // Returns same instance}// Bad: No validation on updatesupdateProfile(updates: any): Host {return new Host({...this, ...updates}); // Bypasses create() validation}
Property Declaration Patterns
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
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 propertiesexport class Host extends BaseEntity implements IHost {userId: UUID; // Can be reassigneddob?: Date; // Hidden optionalityphoneNumber: number | null; // Nullable instead of Optionaddress: Address; // Mutable reference// Bad: Allows direct mutationsetPhoneNumber(phone: number): void {this.phoneNumber = phone; // Direct assignment}}// Bad: Mixed mutabilityexport class Host extends BaseEntity implements IHost {readonly userId: UUID; // Gooddob: Date | undefined; // Bad: mutable and nullablephoneNumber?: number; // Bad: hidden optionality}
Schema and Entity Optional Property Patterns
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
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 schemaexport const HostSchema = S.Struct({dob: S.DateFromSelf, // Required in schemaphoneNumber: S.Number, // Required in schema});// Bad: Nullable types instead of Optionexport class Host extends BaseEntity implements IHost {readonly dob?: Date; // TypeScript optionalreadonly phoneNumber: number | null; // Nullable typereadonly address: Address | undefined; // Undefined type}// Bad: Mixed patternsexport class Host extends BaseEntity implements IHost {readonly dob: O.Option<Date>; // Goodreadonly phoneNumber?: number; // Bad: inconsistent with schema}
Value Object Instantiation Patterns
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
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 constructorprivate constructor(data: SerializedHost) {super();// Validation happens inside constructor - breaks Effect chainthis.address = new Address(data.address); // No validationthis.socialLinks = new SocialLinks(data.socialLinks); // No validation}// Bad: Separate validation stepsstatic 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 creationhost.validateAddress(); // Separate validation stephost.validateSocialLinks(); // Not in Effect chainreturn host;}));}// Bad: Manual object creation without validationstatic 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
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
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 contextexport class HostError extends Error {constructor(message: string) {super(message); // No structured information}}// Bad: String literal errorsthrow new Error("Host validation failed"); // No context about what failed// Bad: Inconsistent error messagesthrow new Error("Invalid email"); // Different formatthrow new Error("Host phone number bad"); // Inconsistent namingthrow new Error("ERROR: dob wrong"); // No standardization// Bad: No inheritance hierarchyexport class HostValidationError extends Error { // Should extend ValidationErrorconstructor(message: string) {super(message); // Loses base error categorization}}// Bad: No structured parametersexport class HostValidationError extends ValidationError {constructor(message: string) { // No field/value contextsuper(message);}}