Advanced Design Patterns - 1/2
The $12 Million Legacy System That Collapsed Under Its Own Weight
Picture this absolute nightmare: A Fortune 500 financial services company with 15 years of “rapid development” finally hits the wall. Their trading platform—handling billions in transactions daily—starts cracking under the pressure of its own architectural sins.
The symptoms were brutal:
- Code changes taking 6 months: Simple feature requests required touching 47 different files across 12 modules
- Bug fixes creating 3 new bugs: Every fix introduced cascading failures in seemingly unrelated systems
- Database deadlocks during peak hours: Their “efficient” data access layer was hammering the DB with thousands of redundant queries
- Testing nightmares: Unit tests were impossible because everything was tightly coupled to everything else
- Developer exodus: Senior engineers were fleeing faster than a sinking ship, leaving behind a knowledge vacuum
- Deployment terror: Every release was a three-day event with rollback plans and emergency war rooms
The root cause? Fifteen years of developers who thought design patterns were “over-engineering” and that “getting it done fast” was more important than getting it done right.
Here’s what they discovered during their six-month emergency refactoring:
- No separation of concerns: Business logic was scattered across controllers, services, data access layers, and even UI components
- Tight coupling everywhere: Changing one class required recompiling half the application
- No consistent data access strategy: 47 different ways to talk to the database, each with its own bugs
- Copy-paste architecture: The same object creation logic existed in 200+ places with slight variations
- Event handling chaos: State changes triggered unpredictable cascades of side effects
- Impossible to test: Dependencies were hardcoded, making unit testing a fantasy
By the time they brought in a team of senior architects to fix the mess:
- $12 million in lost productivity over two years
- 40% of their development team had quit in frustration
- 6 months of feature development completely stopped while they refactored
- 18 months to implement what should have been 3 months of work
- Complete rewrite of 60% of their codebase
The brutal truth? Every single one of these problems could have been avoided with proper implementation of fundamental design patterns that every senior developer should know by heart.
The Uncomfortable Truth About Design Patterns
Here’s what separates codebases that scale gracefully from those that collapse into unmaintainable nightmares: Design patterns aren’t academic exercises—they’re battle-tested solutions to the exact problems that will destroy your application as it grows.
Most developers approach design patterns like this:
- Learn the names and definitions from a textbook or blog post
- Think they’re “over-engineering” for their “simple” application
- Write tightly coupled code because “it’s faster to implement”
- Hit scalability walls and start having PTSD flashbacks about that patterns book
- Spend months refactoring what could have been built right the first time
But developers who build systems that actually work in production think differently:
- Recognize that every codebase will grow beyond its initial scope
- Implement fundamental patterns from day one as insurance against future complexity
- Use patterns not because they’re “fancy,” but because they prevent specific classes of bugs
- Build systems where adding features makes the codebase stronger, not more fragile
- Sleep well at night knowing their code can handle whatever requirements come next
The difference isn’t just code quality—it’s the difference between systems that get better with age and systems that slowly strangle your team’s productivity until you’re spending more time debugging than building.
Ready to build applications like Netflix’s recommendation engine instead of that legacy system that makes senior engineers update their LinkedIn profiles? Let’s dive into the design patterns that form the foundation of maintainable software architecture.
Repository and Unit of Work Patterns: Taming Data Access Chaos
The Problem: Database Logic Scattered Everywhere
// The nightmare every developer inherits at some point
class UserController {
async getUserProfile(req: Request, res: Response) {
// Business logic mixed with data access - RED FLAG #1
const userId = req.params.id;
// Direct database queries in controllers - RED FLAG #2
const userQuery = `
SELECT u.*, p.avatar_url, p.bio, COUNT(f.id) as follower_count
FROM users u
LEFT JOIN profiles p ON u.id = p.user_id
LEFT JOIN followers f ON u.id = f.following_id
WHERE u.id = ? AND u.deleted_at IS NULL
GROUP BY u.id
`;
const user = await db.query(userQuery, [userId]);
if (!user) {
return res.status(404).json({ error: "User not found" });
}
// More queries scattered throughout - RED FLAG #3
const postsQuery = `
SELECT * FROM posts
WHERE user_id = ? AND published = true
ORDER BY created_at DESC LIMIT 10
`;
const posts = await db.query(postsQuery, [userId]);
// Transaction logic mixed with presentation logic - RED FLAG #4
await db.query("UPDATE users SET last_accessed = NOW() WHERE id = ?", [
userId,
]);
res.json({ user, posts });
}
async updateUserProfile(req: Request, res: Response) {
const { userId, email, name, bio } = req.body;
// No transaction handling - RED FLAG #5
await db.query("UPDATE users SET email = ?, name = ? WHERE id = ?", [
email,
name,
userId,
]);
await db.query("UPDATE profiles SET bio = ? WHERE user_id = ?", [
bio,
userId,
]);
// What happens if the second query fails? DATA CORRUPTION!
res.json({ success: true });
}
async deleteUser(req: Request, res: Response) {
const userId = req.params.id;
// Complex deletion logic repeated everywhere - RED FLAG #6
await db.query("DELETE FROM posts WHERE user_id = ?", [userId]);
await db.query("DELETE FROM comments WHERE user_id = ?", [userId]);
await db.query(
"DELETE FROM followers WHERE user_id = ? OR following_id = ?",
[userId, userId]
);
await db.query("DELETE FROM profiles WHERE user_id = ?", [userId]);
await db.query("DELETE FROM users WHERE id = ?", [userId]);
// If any of these fail, you've got partial deletions and orphaned data
res.json({ success: true });
}
}
The Solution: Repository and Unit of Work Patterns
// Domain entity with business logic
export class User {
constructor(
public readonly id: string,
public email: string,
public name: string,
public createdAt: Date,
public deletedAt?: Date
) {}
isActive(): boolean {
return this.deletedAt === null;
}
canBeDeleted(): boolean {
return this.isActive();
}
markAsDeleted(): void {
if (!this.canBeDeleted()) {
throw new Error("User cannot be deleted in current state");
}
this.deletedAt = new Date();
}
updateEmail(newEmail: string): void {
if (!this.isValidEmail(newEmail)) {
throw new Error("Invalid email format");
}
this.email = newEmail;
}
private isValidEmail(email: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
}
export class UserProfile {
constructor(
public readonly userId: string,
public avatarUrl: string,
public bio: string,
public followerCount: number = 0
) {}
updateBio(newBio: string): void {
if (newBio.length > 500) {
throw new Error("Bio cannot exceed 500 characters");
}
this.bio = newBio;
}
}
// Repository interface for dependency inversion
export interface IUserRepository {
findById(id: string): Promise<User | null>;
findByEmail(email: string): Promise<User | null>;
save(user: User): Promise<void>;
delete(user: User): Promise<void>;
findActiveUsers(limit: number): Promise<User[]>;
}
export interface IUserProfileRepository {
findByUserId(userId: string): Promise<UserProfile | null>;
save(profile: UserProfile): Promise<void>;
delete(profile: UserProfile): Promise<void>;
}
// Concrete repository implementation
export class PostgresUserRepository implements IUserRepository {
constructor(private db: Database, private unitOfWork: IUnitOfWork) {}
async findById(id: string): Promise<User | null> {
const query = `
SELECT id, email, name, created_at, deleted_at
FROM users
WHERE id = $1
`;
const result = await this.db.query(query, [id]);
if (!result.rows[0]) {
return null;
}
const row = result.rows[0];
return new User(
row.id,
row.email,
row.name,
row.created_at,
row.deleted_at
);
}
async findByEmail(email: string): Promise<User | null> {
const query = `
SELECT id, email, name, created_at, deleted_at
FROM users
WHERE email = $1 AND deleted_at IS NULL
`;
const result = await this.db.query(query, [email]);
return result.rows[0] ? this.mapToUser(result.rows[0]) : null;
}
async save(user: User): Promise<void> {
const existingUser = await this.findById(user.id);
if (existingUser) {
// Update existing user
this.unitOfWork.registerDirty(user, async () => {
const query = `
UPDATE users
SET email = $1, name = $2, deleted_at = $3
WHERE id = $4
`;
await this.db.query(query, [
user.email,
user.name,
user.deletedAt,
user.id,
]);
});
} else {
// Create new user
this.unitOfWork.registerNew(user, async () => {
const query = `
INSERT INTO users (id, email, name, created_at, deleted_at)
VALUES ($1, $2, $3, $4, $5)
`;
await this.db.query(query, [
user.id,
user.email,
user.name,
user.createdAt,
user.deletedAt,
]);
});
}
}
async delete(user: User): Promise<void> {
// Soft delete approach
user.markAsDeleted();
await this.save(user);
}
async findActiveUsers(limit: number): Promise<User[]> {
const query = `
SELECT id, email, name, created_at, deleted_at
FROM users
WHERE deleted_at IS NULL
ORDER BY created_at DESC
LIMIT $1
`;
const result = await this.db.query(query, [limit]);
return result.rows.map((row) => this.mapToUser(row));
}
private mapToUser(row: any): User {
return new User(
row.id,
row.email,
row.name,
row.created_at,
row.deleted_at
);
}
}
// Unit of Work pattern for transaction management
export interface IUnitOfWork {
registerNew(entity: any, insertAction: () => Promise<void>): void;
registerDirty(entity: any, updateAction: () => Promise<void>): void;
registerDeleted(entity: any, deleteAction: () => Promise<void>): void;
commit(): Promise<void>;
rollback(): Promise<void>;
}
export class DatabaseUnitOfWork implements IUnitOfWork {
private newEntities: Map<any, () => Promise<void>> = new Map();
private dirtyEntities: Map<any, () => Promise<void>> = new Map();
private deletedEntities: Map<any, () => Promise<void>> = new Map();
private transaction: DatabaseTransaction | null = null;
constructor(private db: Database) {}
registerNew(entity: any, insertAction: () => Promise<void>): void {
this.newEntities.set(entity, insertAction);
}
registerDirty(entity: any, updateAction: () => Promise<void>): void {
this.dirtyEntities.set(entity, updateAction);
}
registerDeleted(entity: any, deleteAction: () => Promise<void>): void {
this.deletedEntities.set(entity, deleteAction);
}
async commit(): Promise<void> {
if (this.hasChanges()) {
this.transaction = await this.db.beginTransaction();
try {
// Execute all database operations within a single transaction
await this.executeActions(this.newEntities, "INSERT");
await this.executeActions(this.dirtyEntities, "UPDATE");
await this.executeActions(this.deletedEntities, "DELETE");
await this.transaction.commit();
this.clearChanges();
} catch (error) {
await this.transaction.rollback();
throw new Error(`Transaction failed: ${error.message}`);
}
}
}
async rollback(): Promise<void> {
if (this.transaction) {
await this.transaction.rollback();
this.clearChanges();
}
}
private async executeActions(
actions: Map<any, () => Promise<void>>,
operation: string
): Promise<void> {
for (const [entity, action] of actions) {
try {
await action();
console.log(
`${operation} operation completed for entity:`,
entity.constructor.name
);
} catch (error) {
throw new Error(
`${operation} failed for ${entity.constructor.name}: ${error.message}`
);
}
}
}
private hasChanges(): boolean {
return (
this.newEntities.size > 0 ||
this.dirtyEntities.size > 0 ||
this.deletedEntities.size > 0
);
}
private clearChanges(): void {
this.newEntities.clear();
this.dirtyEntities.clear();
this.deletedEntities.clear();
}
}
// Application service that orchestrates business operations
export class UserService {
constructor(
private userRepository: IUserRepository,
private profileRepository: IUserProfileRepository,
private unitOfWork: IUnitOfWork
) {}
async getUserProfile(userId: string): Promise<UserProfileDto | null> {
const user = await this.userRepository.findById(userId);
if (!user || !user.isActive()) {
return null;
}
const profile = await this.profileRepository.findByUserId(userId);
return {
user: {
id: user.id,
email: user.email,
name: user.name,
createdAt: user.createdAt,
},
profile: profile
? {
avatarUrl: profile.avatarUrl,
bio: profile.bio,
followerCount: profile.followerCount,
}
: null,
};
}
async updateUserProfile(
userId: string,
updateData: { email?: string; name?: string; bio?: string }
): Promise<void> {
const user = await this.userRepository.findById(userId);
if (!user || !user.isActive()) {
throw new Error("User not found or inactive");
}
const profile = await this.profileRepository.findByUserId(userId);
if (!profile) {
throw new Error("User profile not found");
}
// Apply business logic through domain entities
if (updateData.email) {
user.updateEmail(updateData.email);
}
if (updateData.name) {
user.name = updateData.name;
}
if (updateData.bio) {
profile.updateBio(updateData.bio);
}
// Register changes with Unit of Work
await this.userRepository.save(user);
await this.profileRepository.save(profile);
// Commit all changes in a single transaction
await this.unitOfWork.commit();
}
async deleteUserCompletely(userId: string): Promise<void> {
const user = await this.userRepository.findById(userId);
if (!user) {
throw new Error("User not found");
}
if (!user.canBeDeleted()) {
throw new Error("User cannot be deleted in current state");
}
// All related data deletion handled through repositories
const profile = await this.profileRepository.findByUserId(userId);
if (profile) {
await this.profileRepository.delete(profile);
}
await this.userRepository.delete(user);
// Single transaction ensures data consistency
await this.unitOfWork.commit();
}
}
// Clean controller that focuses only on HTTP concerns
export class UserController {
constructor(private userService: UserService) {}
async getUserProfile(req: Request, res: Response): Promise<void> {
try {
const userId = req.params.id;
const profile = await this.userService.getUserProfile(userId);
if (!profile) {
res.status(404).json({ error: "User not found" });
return;
}
res.json(profile);
} catch (error) {
res.status(500).json({ error: "Internal server error" });
}
}
async updateUserProfile(req: Request, res: Response): Promise<void> {
try {
const userId = req.params.id;
const updateData = req.body;
await this.userService.updateUserProfile(userId, updateData);
res.json({ success: true });
} catch (error) {
if (error.message.includes("not found")) {
res.status(404).json({ error: error.message });
} else if (error.message.includes("Invalid email")) {
res.status(400).json({ error: error.message });
} else {
res.status(500).json({ error: "Internal server error" });
}
}
}
async deleteUser(req: Request, res: Response): Promise<void> {
try {
const userId = req.params.id;
await this.userService.deleteUserCompletely(userId);
res.json({ success: true });
} catch (error) {
if (error.message.includes("not found")) {
res.status(404).json({ error: error.message });
} else if (error.message.includes("cannot be deleted")) {
res.status(400).json({ error: error.message });
} else {
res.status(500).json({ error: "Internal server error" });
}
}
}
}
// DTO for API responses
interface UserProfileDto {
user: {
id: string;
email: string;
name: string;
createdAt: Date;
};
profile: {
avatarUrl: string;
bio: string;
followerCount: number;
} | null;
}
Dependency Injection and Inversion of Control: Breaking the Coupling Chains
The Problem: Hardcoded Dependencies Create Testing Nightmares
// The tightly coupled nightmare that makes testing impossible
class EmailService {
private smtpClient: SMTPClient;
constructor() {
// Hardcoded dependency - cannot be tested or swapped
this.smtpClient = new SMTPClient({
host: "smtp.gmail.com",
port: 587,
secure: false,
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASS,
},
});
}
async sendEmail(to: string, subject: string, body: string): Promise<void> {
// Directly coupled to SMTP implementation
await this.smtpClient.sendMail({
from: "noreply@company.com",
to,
subject,
html: body,
});
}
}
class UserRegistrationService {
private database: PostgresDatabase;
private emailService: EmailService;
private logger: ConsoleLogger;
constructor() {
// Multiple hardcoded dependencies - testing requires real database and email service
this.database = new PostgresDatabase(process.env.DATABASE_URL);
this.emailService = new EmailService();
this.logger = new ConsoleLogger();
}
async registerUser(userData: CreateUserRequest): Promise<void> {
this.logger.info("Starting user registration");
try {
// Business logic mixed with infrastructure concerns
const user = await this.database.query(
"INSERT INTO users (email, name, password_hash) VALUES ($1, $2, $3) RETURNING id",
[
userData.email,
userData.name,
await bcrypt.hash(userData.password, 10),
]
);
// Can't test email functionality without actually sending emails
await this.emailService.sendEmail(
userData.email,
"Welcome to Our Platform",
`<h1>Welcome ${userData.name}!</h1><p>Your account has been created.</p>`
);
this.logger.info(`User registered successfully: ${user.id}`);
} catch (error) {
this.logger.error("User registration failed:", error);
throw error;
}
}
}
// Testing this is a nightmare
describe("UserRegistrationService", () => {
test("should register user", async () => {
// How do you test this without:
// 1. Setting up a real PostgreSQL database
// 2. Actually sending emails through Gmail SMTP
// 3. Dealing with environment variables
// 4. Network calls that can fail randomly
const service = new UserRegistrationService();
// This test will:
// - Connect to your production database if you're not careful
// - Send actual emails to real email addresses
// - Fail if your internet is down
// - Take forever because of network calls
await service.registerUser({
email: "test@example.com",
name: "Test User",
password: "password123",
});
// How do you verify the user was created without querying the real database?
// How do you verify the email was sent without actually sending it?
});
});
The Solution: Dependency Injection Container and Interface Segregation
// Define clear interfaces for all dependencies
export interface IEmailService {
sendWelcomeEmail(to: string, name: string): Promise<void>;
sendPasswordResetEmail(to: string, resetToken: string): Promise<void>;
}
export interface IUserRepository {
createUser(user: CreateUserDto): Promise<User>;
findByEmail(email: string): Promise<User | null>;
findById(id: string): Promise<User | null>;
}
export interface ILogger {
info(message: string, meta?: any): void;
error(message: string, error?: Error): void;
warn(message: string, meta?: any): void;
}
export interface IPasswordHasher {
hash(password: string): Promise<string>;
verify(password: string, hash: string): Promise<boolean>;
}
// Production implementations
export class SMTPEmailService implements IEmailService {
constructor(private smtpConfig: SMTPConfig) {}
async sendWelcomeEmail(to: string, name: string): Promise<void> {
const client = new SMTPClient(this.smtpConfig);
await client.sendMail({
from: "noreply@company.com",
to,
subject: "Welcome to Our Platform",
html: `<h1>Welcome ${name}!</h1><p>Your account has been created successfully.</p>`,
});
}
async sendPasswordResetEmail(to: string, resetToken: string): Promise<void> {
const client = new SMTPClient(this.smtpConfig);
const resetUrl = `${process.env.APP_URL}/reset-password?token=${resetToken}`;
await client.sendMail({
from: "noreply@company.com",
to,
subject: "Password Reset Request",
html: `<p>Click <a href="${resetUrl}">here</a> to reset your password.</p>`,
});
}
}
export class PostgresUserRepository implements IUserRepository {
constructor(private db: Database) {}
async createUser(userData: CreateUserDto): Promise<User> {
const query = `
INSERT INTO users (id, email, name, password_hash, created_at)
VALUES ($1, $2, $3, $4, $5)
RETURNING *
`;
const userId = uuidv4();
const result = await this.db.query(query, [
userId,
userData.email,
userData.name,
userData.passwordHash,
new Date(),
]);
return this.mapToUser(result.rows[0]);
}
async findByEmail(email: string): Promise<User | null> {
const query = "SELECT * FROM users WHERE email = $1 AND deleted_at IS NULL";
const result = await this.db.query(query, [email]);
return result.rows[0] ? this.mapToUser(result.rows[0]) : null;
}
async findById(id: string): Promise<User | null> {
const query = "SELECT * FROM users WHERE id = $1 AND deleted_at IS NULL";
const result = await this.db.query(query, [id]);
return result.rows[0] ? this.mapToUser(result.rows[0]) : null;
}
private mapToUser(row: any): User {
return new User(row.id, row.email, row.name, row.created_at);
}
}
export class WinstonLogger implements ILogger {
private logger: winston.Logger;
constructor() {
this.logger = winston.createLogger({
level: "info",
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.File({ filename: "error.log", level: "error" }),
new winston.transports.File({ filename: "combined.log" }),
],
});
if (process.env.NODE_ENV !== "production") {
this.logger.add(
new winston.transports.Console({
format: winston.format.simple(),
})
);
}
}
info(message: string, meta?: any): void {
this.logger.info(message, meta);
}
error(message: string, error?: Error): void {
this.logger.error(message, { error: error?.stack });
}
warn(message: string, meta?: any): void {
this.logger.warn(message, meta);
}
}
export class BCryptPasswordHasher implements IPasswordHasher {
constructor(private saltRounds: number = 12) {}
async hash(password: string): Promise<string> {
return bcrypt.hash(password, this.saltRounds);
}
async verify(password: string, hash: string): Promise<boolean> {
return bcrypt.compare(password, hash);
}
}
// Service with injected dependencies
export class UserRegistrationService {
constructor(
private userRepository: IUserRepository,
private emailService: IEmailService,
private logger: ILogger,
private passwordHasher: IPasswordHasher
) {}
async registerUser(userData: CreateUserRequest): Promise<User> {
this.logger.info("Starting user registration", { email: userData.email });
try {
// Check if user already exists
const existingUser = await this.userRepository.findByEmail(
userData.email
);
if (existingUser) {
throw new UserRegistrationError("User with this email already exists");
}
// Hash password using injected service
const passwordHash = await this.passwordHasher.hash(userData.password);
// Create user through repository
const user = await this.userRepository.createUser({
email: userData.email,
name: userData.name,
passwordHash,
});
// Send welcome email through injected service
await this.emailService.sendWelcomeEmail(user.email, user.name);
this.logger.info("User registered successfully", { userId: user.id });
return user;
} catch (error) {
this.logger.error("User registration failed", error as Error);
throw error;
}
}
}
// Dependency injection container
export class DIContainer {
private services = new Map<string, any>();
private factories = new Map<string, () => any>();
register<T>(name: string, factory: () => T): void {
this.factories.set(name, factory);
}
registerSingleton<T>(name: string, factory: () => T): void {
this.register(name, () => {
if (!this.services.has(name)) {
this.services.set(name, factory());
}
return this.services.get(name);
});
}
resolve<T>(name: string): T {
const factory = this.factories.get(name);
if (!factory) {
throw new Error(`Service ${name} not registered`);
}
return factory();
}
resolveAll(): Record<string, any> {
const resolved: Record<string, any> = {};
for (const [name] of this.factories) {
resolved[name] = this.resolve(name);
}
return resolved;
}
}
// Container configuration
export function setupDIContainer(): DIContainer {
const container = new DIContainer();
// Infrastructure services
container.registerSingleton("logger", () => new WinstonLogger());
container.registerSingleton(
"passwordHasher",
() => new BCryptPasswordHasher(12)
);
container.registerSingleton(
"database",
() =>
new PostgresDatabase({
connectionString: process.env.DATABASE_URL,
ssl: process.env.NODE_ENV === "production",
})
);
// Email service with configuration
container.registerSingleton(
"emailService",
() =>
new SMTPEmailService({
host: process.env.SMTP_HOST || "smtp.gmail.com",
port: parseInt(process.env.SMTP_PORT || "587"),
secure: false,
auth: {
user: process.env.EMAIL_USER!,
pass: process.env.EMAIL_PASS!,
},
})
);
// Repository layer
container.register(
"userRepository",
() => new PostgresUserRepository(container.resolve("database"))
);
// Application services
container.register(
"userRegistrationService",
() =>
new UserRegistrationService(
container.resolve("userRepository"),
container.resolve("emailService"),
container.resolve("logger"),
container.resolve("passwordHasher")
)
);
return container;
}
// Test implementations for easy mocking
export class MockEmailService implements IEmailService {
public sentEmails: Array<{ to: string; type: string; data?: any }> = [];
async sendWelcomeEmail(to: string, name: string): Promise<void> {
this.sentEmails.push({ to, type: "welcome", data: { name } });
}
async sendPasswordResetEmail(to: string, resetToken: string): Promise<void> {
this.sentEmails.push({ to, type: "password-reset", data: { resetToken } });
}
getLastEmail() {
return this.sentEmails[this.sentEmails.length - 1];
}
getEmailsForRecipient(email: string) {
return this.sentEmails.filter((e) => e.to === email);
}
}
export class MockUserRepository implements IUserRepository {
public users: User[] = [];
private nextId = 1;
async createUser(userData: CreateUserDto): Promise<User> {
const user = new User(
`user-${this.nextId++}`,
userData.email,
userData.name,
new Date()
);
this.users.push(user);
return user;
}
async findByEmail(email: string): Promise<User | null> {
return this.users.find((u) => u.email === email) || null;
}
async findById(id: string): Promise<User | null> {
return this.users.find((u) => u.id === id) || null;
}
}
export class MockLogger implements ILogger {
public logs: Array<{ level: string; message: string; meta?: any }> = [];
info(message: string, meta?: any): void {
this.logs.push({ level: "info", message, meta });
}
error(message: string, error?: Error): void {
this.logs.push({ level: "error", message, meta: { error } });
}
warn(message: string, meta?: any): void {
this.logs.push({ level: "warn", message, meta });
}
}
export class MockPasswordHasher implements IPasswordHasher {
async hash(password: string): Promise<string> {
return `hashed_${password}`;
}
async verify(password: string, hash: string): Promise<boolean> {
return hash === `hashed_${password}`;
}
}
// Now testing is a breeze
describe("UserRegistrationService", () => {
let userRegistrationService: UserRegistrationService;
let mockUserRepository: MockUserRepository;
let mockEmailService: MockEmailService;
let mockLogger: MockLogger;
let mockPasswordHasher: MockPasswordHasher;
beforeEach(() => {
// Set up test doubles
mockUserRepository = new MockUserRepository();
mockEmailService = new MockEmailService();
mockLogger = new MockLogger();
mockPasswordHasher = new MockPasswordHasher();
// Inject test doubles
userRegistrationService = new UserRegistrationService(
mockUserRepository,
mockEmailService,
mockLogger,
mockPasswordHasher
);
});
test("should successfully register a new user", async () => {
const userData: CreateUserRequest = {
email: "test@example.com",
name: "Test User",
password: "password123",
};
const user = await userRegistrationService.registerUser(userData);
// Verify user was created
expect(user.email).toBe("test@example.com");
expect(user.name).toBe("Test User");
// Verify user was saved to repository
const savedUser = await mockUserRepository.findByEmail("test@example.com");
expect(savedUser).toBeTruthy();
// Verify welcome email was sent
expect(mockEmailService.sentEmails).toHaveLength(1);
expect(mockEmailService.getLastEmail()).toEqual({
to: "test@example.com",
type: "welcome",
data: { name: "Test User" },
});
// Verify password was hashed
expect(mockPasswordHasher.hash).toHaveBeenCalledWith("password123");
// Verify logging occurred
expect(
mockLogger.logs.some(
(log) =>
log.level === "info" &&
log.message.includes("registered successfully")
)
).toBe(true);
});
test("should reject duplicate email registration", async () => {
// Set up existing user
await mockUserRepository.createUser({
email: "existing@example.com",
name: "Existing User",
passwordHash: "hashed_password",
});
const userData: CreateUserRequest = {
email: "existing@example.com",
name: "New User",
password: "password123",
};
await expect(
userRegistrationService.registerUser(userData)
).rejects.toThrow("User with this email already exists");
// Verify no email was sent
expect(mockEmailService.sentEmails).toHaveLength(0);
// Verify error was logged
expect(
mockLogger.logs.some(
(log) =>
log.level === "error" && log.message.includes("registration failed")
)
).toBe(true);
});
test("should handle email service failure gracefully", async () => {
// Make email service fail
mockEmailService.sendWelcomeEmail = jest
.fn()
.mockRejectedValue(new Error("SMTP server down"));
const userData: CreateUserRequest = {
email: "test@example.com",
name: "Test User",
password: "password123",
};
await expect(
userRegistrationService.registerUser(userData)
).rejects.toThrow("SMTP server down");
// Verify user was still created (depending on your business logic)
const users = mockUserRepository.users;
expect(users).toHaveLength(1);
// Verify error was logged
expect(mockLogger.logs.some((log) => log.level === "error")).toBe(true);
});
});
Factory and Builder Patterns: Mastering Object Creation Complexity
The Problem: Object Creation Logic Scattered Everywhere
// The nightmare of complex object creation scattered throughout your codebase
class ReportGenerator {
generateUserReport(userId: string, reportType: string, options: any) {
let report;
// Object creation logic mixed with business logic - RED FLAG #1
if (reportType === "daily") {
report = {
id: uuidv4(),
type: "daily",
userId,
title: "Daily Activity Report",
createdAt: new Date(),
sections: [
{ name: "summary", template: "daily-summary" },
{ name: "activities", template: "activity-list" },
],
formatting: {
pageSize: "A4",
orientation: "portrait",
margins: { top: 20, bottom: 20, left: 15, right: 15 },
},
filters: {
dateRange: {
start: new Date(Date.now() - 24 * 60 * 60 * 1000),
end: new Date(),
},
},
};
} else if (reportType === "weekly") {
report = {
id: uuidv4(),
type: "weekly",
userId,
title: "Weekly Performance Report",
createdAt: new Date(),
sections: [
{ name: "summary", template: "weekly-summary" },
{ name: "performance", template: "performance-chart" },
{ name: "trends", template: "trend-analysis" },
],
formatting: {
pageSize: "A4",
orientation: "landscape", // Different from daily
margins: { top: 15, bottom: 15, left: 10, right: 10 },
},
filters: {
dateRange: {
start: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
end: new Date(),
},
},
};
} else if (reportType === "monthly") {
// Even more complex creation logic...
report = {
id: uuidv4(),
type: "monthly",
userId,
title: "Monthly Comprehensive Report",
createdAt: new Date(),
sections: [
{ name: "executive-summary", template: "exec-summary" },
{ name: "detailed-metrics", template: "detailed-metrics" },
{ name: "comparisons", template: "month-over-month" },
{ name: "projections", template: "future-projections" },
],
formatting: {
pageSize: "A4",
orientation: "portrait",
margins: { top: 25, bottom: 25, left: 20, right: 20 },
},
filters: {
dateRange: {
start: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
end: new Date(),
},
includeComparisons: true,
includeProjections: options.includeProjections || false,
},
};
} else {
throw new Error("Unknown report type");
}
// More scattered object creation based on user preferences
if (options.customTitle) {
report.title = options.customTitle;
}
if (options.exportFormat === "pdf") {
report.exportSettings = {
format: "pdf",
quality: "high",
embedFonts: true,
};
} else if (options.exportFormat === "excel") {
report.exportSettings = {
format: "xlsx",
includeCharts: true,
sheetNames: ["Summary", "Data", "Charts"],
};
}
return report;
}
}
// Every class that needs to create reports duplicates this logic
class ScheduledReportService {
createScheduledReport(schedule: any) {
// Duplicate object creation logic - RED FLAG #2
let report;
if (schedule.reportType === "daily") {
// Copy-pasted from ReportGenerator with slight variations
report = {
id: uuidv4(),
type: "daily",
userId: schedule.userId,
title: "Scheduled Daily Report",
createdAt: new Date(),
// ... more duplicated code
};
}
// ... more duplication
}
}
The Solution: Factory and Builder Patterns
// Domain models with clear structure
export class Report {
constructor(
public readonly id: string,
public readonly type: ReportType,
public readonly userId: string,
public title: string,
public readonly createdAt: Date,
public sections: ReportSection[],
public formatting: ReportFormatting,
public filters: ReportFilters,
public exportSettings?: ExportSettings
) {}
addSection(section: ReportSection): void {
this.sections.push(section);
}
updateTitle(newTitle: string): void {
if (!newTitle.trim()) {
throw new Error("Report title cannot be empty");
}
this.title = newTitle;
}
canExport(): boolean {
return this.sections.length > 0;
}
}
export class ReportSection {
constructor(
public name: string,
public template: string,
public data?: any,
public visible: boolean = true
) {}
}
export class ReportFormatting {
constructor(
public pageSize: PageSize,
public orientation: PageOrientation,
public margins: Margins
) {}
}
export class ReportFilters {
constructor(
public dateRange: DateRange,
public includeComparisons: boolean = false,
public includeProjections: boolean = false,
public customFilters: Record<string, any> = {}
) {}
}
export class ExportSettings {
constructor(
public format: ExportFormat,
public quality?: string,
public embedFonts?: boolean,
public includeCharts?: boolean,
public sheetNames?: string[]
) {}
}
// Types for type safety
type ReportType = "daily" | "weekly" | "monthly" | "quarterly" | "annual";
type PageSize = "A4" | "A3" | "Letter" | "Legal";
type PageOrientation = "portrait" | "landscape";
type ExportFormat = "pdf" | "xlsx" | "csv" | "html";
interface Margins {
top: number;
bottom: number;
left: number;
right: number;
}
interface DateRange {
start: Date;
end: Date;
}
// Abstract Factory for creating different report types
export abstract class ReportFactory {
abstract createReport(
userId: string,
options?: ReportCreationOptions
): Report;
protected createBaseReport(
type: ReportType,
userId: string,
title: string
): Report {
return new Report(
uuidv4(),
type,
userId,
title,
new Date(),
[],
this.getDefaultFormatting(type),
this.getDefaultFilters(type)
);
}
protected abstract getDefaultFormatting(type: ReportType): ReportFormatting;
protected abstract getDefaultFilters(type: ReportType): ReportFilters;
}
// Concrete factories for specific report types
export class DailyReportFactory extends ReportFactory {
createReport(userId: string, options?: ReportCreationOptions): Report {
const report = this.createBaseReport(
"daily",
userId,
"Daily Activity Report"
);
// Add daily-specific sections
report.addSection(new ReportSection("summary", "daily-summary"));
report.addSection(new ReportSection("activities", "activity-list"));
// Apply custom options
if (options?.customTitle) {
report.updateTitle(options.customTitle);
}
if (options?.exportSettings) {
report.exportSettings = options.exportSettings;
}
return report;
}
protected getDefaultFormatting(type: ReportType): ReportFormatting {
return new ReportFormatting("A4", "portrait", {
top: 20,
bottom: 20,
left: 15,
right: 15,
});
}
protected getDefaultFilters(type: ReportType): ReportFilters {
return new ReportFilters({
start: new Date(Date.now() - 24 * 60 * 60 * 1000),
end: new Date(),
});
}
}
export class WeeklyReportFactory extends ReportFactory {
createReport(userId: string, options?: ReportCreationOptions): Report {
const report = this.createBaseReport(
"weekly",
userId,
"Weekly Performance Report"
);
// Add weekly-specific sections
report.addSection(new ReportSection("summary", "weekly-summary"));
report.addSection(new ReportSection("performance", "performance-chart"));
report.addSection(new ReportSection("trends", "trend-analysis"));
// Apply custom options
if (options?.customTitle) {
report.updateTitle(options.customTitle);
}
if (options?.exportSettings) {
report.exportSettings = options.exportSettings;
}
return report;
}
protected getDefaultFormatting(type: ReportType): ReportFormatting {
return new ReportFormatting(
"A4",
"landscape", // Better for charts and tables
{ top: 15, bottom: 15, left: 10, right: 10 }
);
}
protected getDefaultFilters(type: ReportType): ReportFilters {
return new ReportFilters({
start: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
end: new Date(),
});
}
}
export class MonthlyReportFactory extends ReportFactory {
createReport(userId: string, options?: ReportCreationOptions): Report {
const report = this.createBaseReport(
"monthly",
userId,
"Monthly Comprehensive Report"
);
// Add monthly-specific sections
report.addSection(new ReportSection("executive-summary", "exec-summary"));
report.addSection(
new ReportSection("detailed-metrics", "detailed-metrics")
);
report.addSection(new ReportSection("comparisons", "month-over-month"));
// Conditionally add projections section
if (options?.includeProjections) {
report.addSection(new ReportSection("projections", "future-projections"));
}
// Apply custom options
if (options?.customTitle) {
report.updateTitle(options.customTitle);
}
if (options?.exportSettings) {
report.exportSettings = options.exportSettings;
}
return report;
}
protected getDefaultFormatting(type: ReportType): ReportFormatting {
return new ReportFormatting("A4", "portrait", {
top: 25,
bottom: 25,
left: 20,
right: 20,
});
}
protected getDefaultFilters(type: ReportType): ReportFilters {
return new ReportFilters(
{
start: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
end: new Date(),
},
true, // includeComparisons
false // includeProjections (can be overridden)
);
}
}
// Factory registry for managing multiple factories
export class ReportFactoryRegistry {
private factories = new Map<ReportType, ReportFactory>();
constructor() {
this.registerFactory("daily", new DailyReportFactory());
this.registerFactory("weekly", new WeeklyReportFactory());
this.registerFactory("monthly", new MonthlyReportFactory());
}
registerFactory(type: ReportType, factory: ReportFactory): void {
this.factories.set(type, factory);
}
createReport(
type: ReportType,
userId: string,
options?: ReportCreationOptions
): Report {
const factory = this.factories.get(type);
if (!factory) {
throw new Error(`No factory registered for report type: ${type}`);
}
return factory.createReport(userId, options);
}
getSupportedTypes(): ReportType[] {
return Array.from(this.factories.keys());
}
}
// Builder pattern for complex report configuration
export class ReportBuilder {
private report?: Report;
constructor(private factoryRegistry: ReportFactoryRegistry) {}
forUser(userId: string): ReportBuilder {
this.userId = userId;
return this;
}
ofType(type: ReportType): ReportBuilder {
this.reportType = type;
return this;
}
withTitle(title: string): ReportBuilder {
this.customTitle = title;
return this;
}
withDateRange(start: Date, end: Date): ReportBuilder {
this.dateRange = { start, end };
return this;
}
withSection(name: string, template: string, data?: any): ReportBuilder {
if (!this.additionalSections) {
this.additionalSections = [];
}
this.additionalSections.push(new ReportSection(name, template, data));
return this;
}
withFormatting(formatting: Partial<ReportFormatting>): ReportBuilder {
this.customFormatting = formatting;
return this;
}
withExportSettings(settings: ExportSettings): ReportBuilder {
this.exportSettings = settings;
return this;
}
includeComparisons(include: boolean = true): ReportBuilder {
this.shouldIncludeComparisons = include;
return this;
}
includeProjections(include: boolean = true): ReportBuilder {
this.shouldIncludeProjections = include;
return this;
}
build(): Report {
if (!this.userId || !this.reportType) {
throw new Error("UserId and reportType are required to build a report");
}
const options: ReportCreationOptions = {
customTitle: this.customTitle,
exportSettings: this.exportSettings,
includeProjections: this.shouldIncludeProjections,
};
// Create base report using factory
const report = this.factoryRegistry.createReport(
this.reportType,
this.userId,
options
);
// Apply builder-specific customizations
if (this.dateRange) {
report.filters = new ReportFilters(
this.dateRange,
this.shouldIncludeComparisons ?? report.filters.includeComparisons,
this.shouldIncludeProjections ?? report.filters.includeProjections
);
}
if (this.customFormatting) {
report.formatting = Object.assign(
report.formatting,
this.customFormatting
);
}
if (this.additionalSections) {
this.additionalSections.forEach((section) => report.addSection(section));
}
return report;
}
// Private state for builder
private userId?: string;
private reportType?: ReportType;
private customTitle?: string;
private dateRange?: DateRange;
private additionalSections?: ReportSection[];
private customFormatting?: Partial<ReportFormatting>;
private exportSettings?: ExportSettings;
private shouldIncludeComparisons?: boolean;
private shouldIncludeProjections?: boolean;
}
// Clean service layer using factories and builders
export class ReportService {
constructor(private factoryRegistry: ReportFactoryRegistry) {}
// Simple factory usage for standard reports
generateStandardReport(type: ReportType, userId: string): Report {
return this.factoryRegistry.createReport(type, userId);
}
// Builder usage for complex custom reports
generateCustomReport(): ReportBuilder {
return new ReportBuilder(this.factoryRegistry);
}
// Example business methods using the patterns
async generateWeeklyPerformanceReport(userId: string): Promise<Report> {
return this.generateCustomReport()
.forUser(userId)
.ofType("weekly")
.withTitle("Weekly Performance Analysis")
.withDateRange(new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), new Date())
.withExportSettings(new ExportSettings("pdf", "high", true))
.includeComparisons(true)
.build();
}
async generateExecutiveReport(
userId: string,
customSections: ReportSection[]
): Promise<Report> {
const builder = this.generateCustomReport()
.forUser(userId)
.ofType("monthly")
.withTitle("Executive Summary Report")
.withFormatting({
pageSize: "A3",
orientation: "landscape",
})
.includeComparisons(true)
.includeProjections(true);
// Add custom sections dynamically
customSections.forEach((section) => {
builder.withSection(section.name, section.template, section.data);
});
return builder.build();
}
}
// Usage examples showing the power of these patterns
interface ReportCreationOptions {
customTitle?: string;
exportSettings?: ExportSettings;
includeProjections?: boolean;
}
// Clean controller using the service
export class ReportController {
constructor(private reportService: ReportService) {}
async generateReport(req: Request, res: Response): Promise<void> {
try {
const { type, userId, options } = req.body;
let report: Report;
if (options && Object.keys(options).length > 0) {
// Use builder for custom reports
const builder = this.reportService
.generateCustomReport()
.forUser(userId)
.ofType(type);
if (options.title) builder.withTitle(options.title);
if (options.dateRange)
builder.withDateRange(options.dateRange.start, options.dateRange.end);
if (options.exportFormat) {
builder.withExportSettings(new ExportSettings(options.exportFormat));
}
report = builder.build();
} else {
// Use factory for standard reports
report = this.reportService.generateStandardReport(type, userId);
}
res.json({
reportId: report.id,
type: report.type,
title: report.title,
sections: report.sections.length,
canExport: report.canExport(),
});
} catch (error) {
res.status(400).json({ error: error.message });
}
}
}
Observer and Strategy Patterns: Flexible Behavior and Event Handling
The Problem: Tight Coupling Between Components and Inflexible Algorithms
// The nightmare of tightly coupled event handling and hardcoded algorithms
class UserService {
private emailService = new EmailService();
private analyticsService = new AnalyticsService();
private notificationService = new NotificationService();
private auditService = new AuditService();
async createUser(userData: CreateUserRequest): Promise<User> {
const user = new User(userData.email, userData.name);
await this.saveUser(user);
// Tightly coupled side effects - RED FLAG #1
// What if EmailService is down? The whole operation fails
await this.emailService.sendWelcomeEmail(user.email, user.name);
// What if we need to send different emails based on user type?
// More if/else logic scattered everywhere
if (user.isPremium()) {
await this.emailService.sendPremiumWelcomeEmail(user.email, user.name);
}
// What if AnalyticsService takes 30 seconds to respond?
await this.analyticsService.trackUserRegistration(user);
// Adding new side effects requires modifying this core business method
await this.notificationService.sendAdminNotification(
"New user registered",
user
);
await this.auditService.logUserCreation(user);
return user;
}
async deleteUser(userId: string): Promise<void> {
const user = await this.findUser(userId);
// Hardcoded deletion logic - RED FLAG #2
await this.deleteUserPosts(userId);
await this.deleteUserComments(userId);
await this.deleteUserFollowers(userId);
await this.markUserAsDeleted(userId);
// More tightly coupled side effects
await this.emailService.sendGoodbyeEmail(user.email, user.name);
await this.analyticsService.trackUserDeletion(user);
await this.auditService.logUserDeletion(user);
}
}
// Payment processing with hardcoded strategies
class PaymentService {
async processPayment(paymentRequest: PaymentRequest): Promise<PaymentResult> {
// Inflexible algorithm selection - RED FLAG #3
if (paymentRequest.amount < 10) {
// Small payments go through micro-payment processor
return await this.processMicroPayment(paymentRequest);
} else if (paymentRequest.amount < 1000) {
// Standard payments
if (paymentRequest.country === "US") {
return await this.processWithStripe(paymentRequest);
} else if (paymentRequest.country === "EU") {
return await this.processWithPayPal(paymentRequest);
} else {
throw new Error("Unsupported country");
}
} else {
// Large payments need additional verification
if (paymentRequest.isVerified) {
return await this.processLargePayment(paymentRequest);
} else {
throw new Error("Large payments require verification");
}
}
}
// Adding new payment methods requires modifying the core logic
private async processMicroPayment(
request: PaymentRequest
): Promise<PaymentResult> {
// Hardcoded micro-payment logic
}
private async processWithStripe(
request: PaymentRequest
): Promise<PaymentResult> {
// Hardcoded Stripe logic
}
private async processWithPayPal(
request: PaymentRequest
): Promise<PaymentResult> {
// Hardcoded PayPal logic
}
}
The Solution: Observer and Strategy Patterns
// Observer Pattern: Decoupled event handling
export interface IEventObserver<T> {
handle(event: T): Promise<void>;
canHandle(event: T): boolean;
priority: number; // Lower numbers = higher priority
isAsync: boolean; // Can this handler run asynchronously?
}
export class EventPublisher<T> {
private observers: IEventObserver<T>[] = [];
subscribe(observer: IEventObserver<T>): void {
this.observers.push(observer);
// Sort by priority (lower numbers first)
this.observers.sort((a, b) => a.priority - b.priority);
}
unsubscribe(observer: IEventObserver<T>): void {
const index = this.observers.indexOf(observer);
if (index > -1) {
this.observers.splice(index, 1);
}
}
async publish(event: T): Promise<void> {
const syncHandlers = this.observers.filter(
(obs) => obs.canHandle(event) && !obs.isAsync
);
const asyncHandlers = this.observers.filter(
(obs) => obs.canHandle(event) && obs.isAsync
);
// Execute synchronous handlers first (in priority order)
for (const handler of syncHandlers) {
try {
await handler.handle(event);
} catch (error) {
console.error(`Sync handler failed for event:`, error);
// Continue processing other handlers
}
}
// Execute async handlers in parallel (fire and forget)
const asyncPromises = asyncHandlers.map(async (handler) => {
try {
await handler.handle(event);
} catch (error) {
console.error(`Async handler failed for event:`, error);
}
});
// Don't wait for async handlers to complete
Promise.all(asyncPromises).catch(() => {
// Async handler failures are logged but don't affect the main flow
});
}
}
// Event types
export class UserCreatedEvent {
constructor(
public readonly user: User,
public readonly timestamp: Date = new Date(),
public readonly metadata: Record<string, any> = {}
) {}
}
export class UserDeletedEvent {
constructor(
public readonly user: User,
public readonly timestamp: Date = new Date(),
public readonly metadata: Record<string, any> = {}
) {}
}
// Concrete observers for different concerns
export class WelcomeEmailObserver implements IEventObserver<UserCreatedEvent> {
priority = 1; // High priority - users expect immediate welcome emails
isAsync = false; // Run synchronously to ensure email is sent
constructor(private emailService: IEmailService) {}
canHandle(event: UserCreatedEvent): boolean {
return event.user.isActive() && event.user.hasValidEmail();
}
async handle(event: UserCreatedEvent): Promise<void> {
const { user } = event;
if (user.isPremium()) {
await this.emailService.sendPremiumWelcomeEmail(user.email, user.name);
} else {
await this.emailService.sendWelcomeEmail(user.email, user.name);
}
console.log(`Welcome email sent to ${user.email}`);
}
}
export class UserAnalyticsObserver implements IEventObserver<UserCreatedEvent> {
priority = 10; // Lower priority - analytics can wait
isAsync = true; // Run asynchronously - don't block user registration
constructor(private analyticsService: IAnalyticsService) {}
canHandle(event: UserCreatedEvent): boolean {
return true; // Track all user registrations
}
async handle(event: UserCreatedEvent): Promise<void> {
await this.analyticsService.trackUserRegistration(event.user, {
timestamp: event.timestamp,
...event.metadata,
});
console.log(`Analytics tracked for user ${event.user.id}`);
}
}
export class AdminNotificationObserver
implements IEventObserver<UserCreatedEvent>
{
priority = 5; // Medium priority
isAsync = true; // Non-blocking
constructor(
private notificationService: INotificationService,
private minAmountForNotification: number = 1000
) {}
canHandle(event: UserCreatedEvent): boolean {
// Only notify admins for premium users or large accounts
return (
event.user.isPremium() ||
(event.metadata.initialDeposit &&
event.metadata.initialDeposit > this.minAmountForNotification)
);
}
async handle(event: UserCreatedEvent): Promise<void> {
const message = event.user.isPremium()
? `New premium user registered: ${event.user.name}`
: `New high-value user registered: ${event.user.name}`;
await this.notificationService.sendAdminNotification(message, {
userId: event.user.id,
userEmail: event.user.email,
timestamp: event.timestamp,
});
}
}
export class AuditLogObserver
implements IEventObserver<UserCreatedEvent | UserDeletedEvent>
{
priority = 2; // High priority - audit logs are important
isAsync = false; // Synchronous for data integrity
constructor(private auditService: IAuditService) {}
canHandle(event: UserCreatedEvent | UserDeletedEvent): boolean {
return true; // Audit everything
}
async handle(event: UserCreatedEvent | UserDeletedEvent): Promise<void> {
if (event instanceof UserCreatedEvent) {
await this.auditService.logUserCreation(event.user, event.timestamp);
} else if (event instanceof UserDeletedEvent) {
await this.auditService.logUserDeletion(event.user, event.timestamp);
}
}
}
// Strategy Pattern: Flexible algorithm selection
export interface PaymentStrategy {
canProcess(request: PaymentRequest): boolean;
process(request: PaymentRequest): Promise<PaymentResult>;
getProcessingFee(amount: number): number;
getSupportedCountries(): string[];
getName(): string;
}
export class StripePaymentStrategy implements PaymentStrategy {
constructor(private stripeClient: StripeClient) {}
canProcess(request: PaymentRequest): boolean {
return (
request.amount >= 0.5 && // Stripe minimum
request.amount <= 999999.99 && // Stripe maximum
["US", "CA", "UK"].includes(request.country) &&
["card", "bank_transfer"].includes(request.method)
);
}
async process(request: PaymentRequest): Promise<PaymentResult> {
try {
const charge = await this.stripeClient.charges.create({
amount: Math.round(request.amount * 100), // Convert to cents
currency: request.currency,
source: request.paymentMethod,
description: request.description,
});
return new PaymentResult(
charge.id,
"completed",
request.amount,
this.getProcessingFee(request.amount),
"stripe"
);
} catch (error) {
throw new PaymentProcessingError(
`Stripe processing failed: ${error.message}`
);
}
}
getProcessingFee(amount: number): number {
return Math.round((amount * 0.029 + 0.3) * 100) / 100; // 2.9% + $0.30
}
getSupportedCountries(): string[] {
return ["US", "CA", "UK", "AU", "NZ"];
}
getName(): string {
return "Stripe";
}
}
export class PayPalPaymentStrategy implements PaymentStrategy {
constructor(private paypalClient: PayPalClient) {}
canProcess(request: PaymentRequest): boolean {
return (
request.amount >= 1.0 && // PayPal minimum
request.amount <= 10000.0 && // PayPal maximum for non-verified accounts
this.getSupportedCountries().includes(request.country) &&
["paypal", "bank_transfer"].includes(request.method)
);
}
async process(request: PaymentRequest): Promise<PaymentResult> {
try {
const payment = await this.paypalClient.payment.create({
intent: "sale",
payer: {
payment_method: "paypal",
},
transactions: [
{
amount: {
total: request.amount.toString(),
currency: request.currency,
},
description: request.description,
},
],
});
return new PaymentResult(
payment.id,
"pending", // PayPal typically requires user approval
request.amount,
this.getProcessingFee(request.amount),
"paypal"
);
} catch (error) {
throw new PaymentProcessingError(
`PayPal processing failed: ${error.message}`
);
}
}
getProcessingFee(amount: number): number {
return Math.round((amount * 0.034 + 0.49) * 100) / 100; // 3.4% + $0.49
}
getSupportedCountries(): string[] {
return ["US", "CA", "UK", "AU", "DE", "FR", "ES", "IT", "NL", "BE"];
}
getName(): string {
return "PayPal";
}
}
export class CryptoPaymentStrategy implements PaymentStrategy {
constructor(private cryptoGateway: CryptoGateway) {}
canProcess(request: PaymentRequest): boolean {
return (
request.amount >= 10.0 && // Higher minimum for crypto
request.method === "crypto" &&
["BTC", "ETH", "USDC"].includes(request.cryptoCurrency || "")
);
}
async process(request: PaymentRequest): Promise<PaymentResult> {
try {
const transaction = await this.cryptoGateway.createTransaction({
amount: request.amount,
currency: request.cryptoCurrency,
recipient: request.cryptoAddress,
metadata: request.description,
});
return new PaymentResult(
transaction.hash,
"pending", // Crypto transactions need confirmation
request.amount,
this.getProcessingFee(request.amount),
"crypto"
);
} catch (error) {
throw new PaymentProcessingError(
`Crypto processing failed: ${error.message}`
);
}
}
getProcessingFee(amount: number): number {
return 5.0; // Flat fee for crypto transactions
}
getSupportedCountries(): string[] {
// Crypto is global (subject to local regulations)
return ["*"]; // Special marker for global support
}
getName(): string {
return "Crypto Gateway";
}
}
// Strategy context that selects and executes strategies
export class PaymentProcessor {
private strategies: PaymentStrategy[] = [];
constructor() {
// Register default strategies
this.addStrategy(new StripePaymentStrategy(new StripeClient()));
this.addStrategy(new PayPalPaymentStrategy(new PayPalClient()));
this.addStrategy(new CryptoPaymentStrategy(new CryptoGateway()));
}
addStrategy(strategy: PaymentStrategy): void {
this.strategies.push(strategy);
}
removeStrategy(strategyName: string): void {
this.strategies = this.strategies.filter(
(s) => s.getName() !== strategyName
);
}
async processPayment(request: PaymentRequest): Promise<PaymentResult> {
// Find the best strategy for this request
const suitableStrategies = this.strategies.filter((s) =>
s.canProcess(request)
);
if (suitableStrategies.length === 0) {
throw new PaymentProcessingError(
`No suitable payment strategy found for request: ${JSON.stringify(
request
)}`
);
}
// Select strategy with lowest processing fee
const bestStrategy = suitableStrategies.reduce((best, current) => {
const bestFee = best.getProcessingFee(request.amount);
const currentFee = current.getProcessingFee(request.amount);
return currentFee < bestFee ? current : best;
});
console.log(`Processing payment with strategy: ${bestStrategy.getName()}`);
try {
return await bestStrategy.process(request);
} catch (error) {
// Try fallback strategies if the first one fails
for (const fallbackStrategy of suitableStrategies) {
if (fallbackStrategy !== bestStrategy) {
try {
console.log(
`Trying fallback strategy: ${fallbackStrategy.getName()}`
);
return await fallbackStrategy.process(request);
} catch (fallbackError) {
continue; // Try next fallback
}
}
}
// All strategies failed
throw error;
}
}
getSupportedPaymentMethods(country: string): Array<{
strategy: string;
methods: string[];
minAmount: number;
maxAmount: number;
processingFee: string;
}> {
return this.strategies
.filter(
(s) =>
s.getSupportedCountries().includes(country) ||
s.getSupportedCountries().includes("*")
)
.map((strategy) => ({
strategy: strategy.getName(),
methods: this.getMethodsForStrategy(strategy),
minAmount: this.getMinAmountForStrategy(strategy),
maxAmount: this.getMaxAmountForStrategy(strategy),
processingFee: this.getProcessingFeeDescription(strategy),
}));
}
private getMethodsForStrategy(strategy: PaymentStrategy): string[] {
// This would be determined by analyzing what methods the strategy supports
// For simplicity, returning common methods
if (strategy.getName() === "Stripe") return ["card", "bank_transfer"];
if (strategy.getName() === "PayPal") return ["paypal", "bank_transfer"];
if (strategy.getName() === "Crypto Gateway") return ["crypto"];
return [];
}
private getMinAmountForStrategy(strategy: PaymentStrategy): number {
// Would be determined by strategy capabilities
if (strategy.getName() === "Stripe") return 0.5;
if (strategy.getName() === "PayPal") return 1.0;
if (strategy.getName() === "Crypto Gateway") return 10.0;
return 0;
}
private getMaxAmountForStrategy(strategy: PaymentStrategy): number {
// Would be determined by strategy capabilities
if (strategy.getName() === "Stripe") return 999999.99;
if (strategy.getName() === "PayPal") return 10000.0;
if (strategy.getName() === "Crypto Gateway") return 1000000.0;
return 0;
}
private getProcessingFeeDescription(strategy: PaymentStrategy): string {
const testAmount = 100;
const fee = strategy.getProcessingFee(testAmount);
const percentage = ((fee / testAmount) * 100).toFixed(2);
return `~${percentage}% + fixed fees`;
}
}
// Clean service layer using both patterns
export class UserService {
private userCreatedPublisher = new EventPublisher<UserCreatedEvent>();
private userDeletedPublisher = new EventPublisher<UserDeletedEvent>();
constructor(
private userRepository: IUserRepository,
// Observers are injected for flexibility
welcomeEmailObserver: WelcomeEmailObserver,
analyticsObserver: UserAnalyticsObserver,
adminNotificationObserver: AdminNotificationObserver,
auditObserver: AuditLogObserver
) {
// Subscribe observers
this.userCreatedPublisher.subscribe(welcomeEmailObserver);
this.userCreatedPublisher.subscribe(analyticsObserver);
this.userCreatedPublisher.subscribe(adminNotificationObserver);
this.userCreatedPublisher.subscribe(auditObserver);
this.userDeletedPublisher.subscribe(auditObserver);
}
async createUser(
userData: CreateUserRequest,
metadata?: Record<string, any>
): Promise<User> {
// Core business logic - clean and focused
const user = new User(userData.email, userData.name, userData.isPremium);
await this.userRepository.save(user);
// Publish event - all side effects are handled by observers
await this.userCreatedPublisher.publish(
new UserCreatedEvent(user, new Date(), metadata)
);
return user;
}
async deleteUser(userId: string): Promise<void> {
const user = await this.userRepository.findById(userId);
if (!user) {
throw new Error("User not found");
}
// Core business logic
await this.userRepository.delete(user);
// Publish event - all side effects handled by observers
await this.userDeletedPublisher.publish(new UserDeletedEvent(user));
}
}
export class PaymentService {
constructor(private paymentProcessor: PaymentProcessor) {}
async processPayment(request: PaymentRequest): Promise<PaymentResult> {
// Validate request
if (!request.amount || request.amount <= 0) {
throw new PaymentProcessingError("Invalid payment amount");
}
// Strategy pattern handles all the complex algorithm selection
return await this.paymentProcessor.processPayment(request);
}
async getSupportedMethods(country: string): Promise<PaymentMethod[]> {
return this.paymentProcessor.getSupportedPaymentMethods(country);
}
}
// Supporting types
export class PaymentRequest {
constructor(
public amount: number,
public currency: string,
public country: string,
public method: string,
public paymentMethod: string, // Token or payment method ID
public description: string,
public cryptoCurrency?: string,
public cryptoAddress?: string
) {}
}
export class PaymentResult {
constructor(
public id: string,
public status: "completed" | "pending" | "failed",
public amount: number,
public fee: number,
public processor: string
) {}
}
export class User {
constructor(
public email: string,
public name: string,
private premium: boolean = false,
public readonly id: string = uuidv4()
) {}
isPremium(): boolean {
return this.premium;
}
isActive(): boolean {
return true; // Simplified for example
}
hasValidEmail(): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(this.email);
}
}
interface CreateUserRequest {
email: string;
name: string;
isPremium?: boolean;
}
interface PaymentMethod {
strategy: string;
methods: string[];
minAmount: number;
maxAmount: number;
processingFee: string;
}
export class PaymentProcessingError extends Error {
constructor(message: string) {
super(message);
this.name = "PaymentProcessingError";
}
}
Command and Chain of Responsibility Patterns: Orchestrating Complex Operations
The Problem: Complex Business Logic Mixed with Execution Concerns
// The nightmare of complex business operations with no separation of concerns
class OrderProcessingService {
async processOrder(orderData: any) {
// Validation logic mixed with business logic - RED FLAG #1
if (!orderData.customerId) {
throw new Error("Customer ID is required");
}
if (!orderData.items || orderData.items.length === 0) {
throw new Error("Order must have at least one item");
}
let total = 0;
for (const item of orderData.items) {
if (!item.productId || !item.quantity || item.quantity <= 0) {
throw new Error("Invalid item data");
}
total += item.price * item.quantity;
}
// No undo mechanism - RED FLAG #2
// If any step fails, previous steps are not rolled back
// Step 1: Reserve inventory (what if this succeeds but payment fails?)
const reservations = [];
for (const item of orderData.items) {
const reservation = await this.inventoryService.reserve(
item.productId,
item.quantity
);
reservations.push(reservation);
}
try {
// Step 2: Process payment (what if this fails?)
const paymentResult = await this.paymentService.charge(
orderData.customerId,
total
);
if (!paymentResult.success) {
// Manual cleanup required - ERROR PRONE
for (const reservation of reservations) {
await this.inventoryService.release(reservation.id);
}
throw new Error("Payment failed");
}
// Step 3: Create order record (what if database is down?)
const order = await this.orderRepository.create({
customerId: orderData.customerId,
items: orderData.items,
total: total,
paymentId: paymentResult.id,
status: "confirmed",
});
// Step 4: Send confirmation email (what if email service fails?)
await this.emailService.sendOrderConfirmation(
orderData.customerEmail,
order
);
// Step 5: Update analytics (what if this takes forever?)
await this.analyticsService.trackOrderCreation(order);
return order;
} catch (error) {
// Incomplete cleanup - some reservations might remain
try {
for (const reservation of reservations) {
await this.inventoryService.release(reservation.id);
}
} catch (cleanupError) {
console.error("Cleanup failed:", cleanupError);
}
throw error;
}
}
// Similar problems in other methods...
async cancelOrder(orderId: string) {
// No way to reuse cancellation logic
// Manual step-by-step reversal
const order = await this.orderRepository.findById(orderId);
// What if refund succeeds but inventory release fails?
await this.paymentService.refund(order.paymentId);
await this.inventoryService.releaseOrderReservations(order);
await this.orderRepository.markAsCanceled(order.id);
await this.emailService.sendCancellationConfirmation(
order.customerEmail,
order
);
}
}
The Solution: Command and Chain of Responsibility Patterns
// Command Pattern: Encapsulating operations with undo capabilities
export interface ICommand {
execute(): Promise<CommandResult>;
undo(): Promise<void>;
canUndo(): boolean;
getDescription(): string;
getExecutionTime(): number;
}
export class CommandResult {
constructor(
public success: boolean,
public data?: any,
public error?: Error,
public metadata?: Record<string, any>
) {}
static success(data?: any, metadata?: Record<string, any>): CommandResult {
return new CommandResult(true, data, undefined, metadata);
}
static failure(error: Error, metadata?: Record<string, any>): CommandResult {
return new CommandResult(false, undefined, error, metadata);
}
}
// Base command implementation with common functionality
export abstract class BaseCommand implements ICommand {
protected executionTime: number = 0;
protected executionData?: any;
async execute(): Promise<CommandResult> {
const startTime = Date.now();
try {
this.executionData = await this.doExecute();
this.executionTime = Date.now() - startTime;
return CommandResult.success(this.executionData, {
executionTime: this.executionTime,
command: this.getDescription(),
});
} catch (error) {
this.executionTime = Date.now() - startTime;
return CommandResult.failure(error as Error, {
executionTime: this.executionTime,
command: this.getDescription(),
});
}
}
abstract doExecute(): Promise<any>;
abstract undo(): Promise<void>;
abstract getDescription(): string;
canUndo(): boolean {
return this.executionData !== undefined;
}
getExecutionTime(): number {
return this.executionTime;
}
}
// Concrete commands for order processing
export class ReserveInventoryCommand extends BaseCommand {
private reservations: InventoryReservation[] = [];
constructor(
private inventoryService: IInventoryService,
private orderItems: OrderItem[]
) {
super();
}
async doExecute(): Promise<InventoryReservation[]> {
const reservations: InventoryReservation[] = [];
for (const item of this.orderItems) {
try {
const reservation = await this.inventoryService.reserve(
item.productId,
item.quantity
);
reservations.push(reservation);
} catch (error) {
// If any reservation fails, release all previous ones
await this.releaseReservations(reservations);
throw new Error(
`Failed to reserve inventory for product ${item.productId}: ${error.message}`
);
}
}
this.reservations = reservations;
return reservations;
}
async undo(): Promise<void> {
await this.releaseReservations(this.reservations);
this.reservations = [];
}
private async releaseReservations(
reservations: InventoryReservation[]
): Promise<void> {
const releasePromises = reservations.map(async (reservation) => {
try {
await this.inventoryService.release(reservation.id);
} catch (error) {
console.error(
`Failed to release reservation ${reservation.id}:`,
error
);
}
});
await Promise.allSettled(releasePromises);
}
getDescription(): string {
return `Reserve inventory for ${this.orderItems.length} items`;
}
}
export class ProcessPaymentCommand extends BaseCommand {
private paymentResult?: PaymentResult;
constructor(
private paymentService: IPaymentService,
private customerId: string,
private amount: number,
private paymentMethod: string
) {
super();
}
async doExecute(): Promise<PaymentResult> {
this.paymentResult = await this.paymentService.processPayment({
customerId: this.customerId,
amount: this.amount,
paymentMethod: this.paymentMethod,
});
if (!this.paymentResult.success) {
throw new Error(
`Payment processing failed: ${this.paymentResult.errorMessage}`
);
}
return this.paymentResult;
}
async undo(): Promise<void> {
if (this.paymentResult && this.paymentResult.success) {
try {
await this.paymentService.refund(this.paymentResult.transactionId);
} catch (error) {
console.error(
`Failed to refund payment ${this.paymentResult.transactionId}:`,
error
);
throw error;
}
}
}
getDescription(): string {
return `Process payment of $${this.amount} for customer ${this.customerId}`;
}
}
export class CreateOrderCommand extends BaseCommand {
private createdOrder?: Order;
constructor(
private orderRepository: IOrderRepository,
private orderData: CreateOrderData
) {
super();
}
async doExecute(): Promise<Order> {
this.createdOrder = await this.orderRepository.create(this.orderData);
return this.createdOrder;
}
async undo(): Promise<void> {
if (this.createdOrder) {
try {
await this.orderRepository.delete(this.createdOrder.id);
} catch (error) {
console.error(`Failed to delete order ${this.createdOrder.id}:`, error);
throw error;
}
}
}
getDescription(): string {
return `Create order record for customer ${this.orderData.customerId}`;
}
}
export class SendOrderConfirmationCommand extends BaseCommand {
constructor(
private emailService: IEmailService,
private customerEmail: string,
private order: Order
) {
super();
}
async doExecute(): Promise<EmailResult> {
return await this.emailService.sendOrderConfirmation(
this.customerEmail,
this.order
);
}
async undo(): Promise<void> {
// Email cannot be "unsent", but we can log this for audit purposes
console.log(
`Order confirmation email was sent to ${this.customerEmail} for order ${this.order.id}`
);
// Optionally send a cancellation email
}
canUndo(): boolean {
return false; // Emails cannot be undone
}
getDescription(): string {
return `Send order confirmation email to ${this.customerEmail}`;
}
}
// Command executor with transaction-like behavior
export class CommandExecutor {
private executedCommands: ICommand[] = [];
async executeCommands(commands: ICommand[]): Promise<CommandExecutionResult> {
const results: CommandResult[] = [];
const startTime = Date.now();
try {
// Execute commands in order
for (const command of commands) {
console.log(`Executing: ${command.getDescription()}`);
const result = await command.execute();
if (!result.success) {
throw new CommandExecutionError(
`Command failed: ${command.getDescription()}`,
result.error!,
this.executedCommands.slice()
);
}
results.push(result);
this.executedCommands.push(command);
}
return new CommandExecutionResult(
true,
results,
Date.now() - startTime,
this.executedCommands.slice()
);
} catch (error) {
// Roll back all executed commands in reverse order
await this.rollbackCommands();
throw error;
}
}
private async rollbackCommands(): Promise<void> {
console.log("Rolling back executed commands...");
// Rollback in reverse order
const commandsToRollback = [...this.executedCommands].reverse();
for (const command of commandsToRollback) {
if (command.canUndo()) {
try {
console.log(`Rolling back: ${command.getDescription()}`);
await command.undo();
} catch (rollbackError) {
console.error(
`Failed to rollback command: ${command.getDescription()}`,
rollbackError
);
}
}
}
this.executedCommands = [];
}
async rollback(): Promise<void> {
await this.rollbackCommands();
}
}
export class CommandExecutionResult {
constructor(
public success: boolean,
public commandResults: CommandResult[],
public totalExecutionTime: number,
public executedCommands: ICommand[]
) {}
}
export class CommandExecutionError extends Error {
constructor(
message: string,
public originalError: Error,
public executedCommands: ICommand[]
) {
super(message);
this.name = "CommandExecutionError";
}
}
// Chain of Responsibility Pattern: Flexible request processing pipeline
export interface IRequestHandler<TRequest, TResponse> {
handle(request: TRequest): Promise<TResponse | null>;
setNext(
handler: IRequestHandler<TRequest, TResponse>
): IRequestHandler<TRequest, TResponse>;
}
export abstract class BaseRequestHandler<TRequest, TResponse>
implements IRequestHandler<TRequest, TResponse>
{
private nextHandler?: IRequestHandler<TRequest, TResponse>;
setNext(
handler: IRequestHandler<TRequest, TResponse>
): IRequestHandler<TRequest, TResponse> {
this.nextHandler = handler;
return handler;
}
async handle(request: TRequest): Promise<TResponse | null> {
const result = await this.doHandle(request);
if (result !== null) {
return result;
}
if (this.nextHandler) {
return await this.nextHandler.handle(request);
}
return null;
}
protected abstract doHandle(request: TRequest): Promise<TResponse | null>;
}
// Request validation chain
export class OrderValidationRequest {
constructor(
public orderData: any,
public customer: Customer,
public validationContext: ValidationContext
) {}
}
export class OrderValidationResponse {
constructor(
public isValid: boolean,
public errors: ValidationError[] = [],
public warnings: ValidationWarning[] = [],
public normalizedData?: any
) {}
}
export class CustomerValidationHandler extends BaseRequestHandler<
OrderValidationRequest,
OrderValidationResponse
> {
protected async doHandle(
request: OrderValidationRequest
): Promise<OrderValidationResponse | null> {
const errors: ValidationError[] = [];
// Validate customer exists and is active
if (!request.customer) {
errors.push(new ValidationError("customer", "Customer not found"));
} else if (!request.customer.isActive) {
errors.push(
new ValidationError("customer", "Customer account is inactive")
);
} else if (request.customer.isBlocked) {
errors.push(
new ValidationError("customer", "Customer account is blocked")
);
}
if (errors.length > 0) {
return new OrderValidationResponse(false, errors);
}
// Continue to next handler if customer validation passes
return null;
}
}
export class OrderItemsValidationHandler extends BaseRequestHandler<
OrderValidationRequest,
OrderValidationResponse
> {
constructor(private productService: IProductService) {
super();
}
protected async doHandle(
request: OrderValidationRequest
): Promise<OrderValidationResponse | null> {
const errors: ValidationError[] = [];
const warnings: ValidationWarning[] = [];
if (!request.orderData.items || !Array.isArray(request.orderData.items)) {
errors.push(new ValidationError("items", "Order must contain items"));
return new OrderValidationResponse(false, errors);
}
if (request.orderData.items.length === 0) {
errors.push(
new ValidationError("items", "Order must contain at least one item")
);
return new OrderValidationResponse(false, errors);
}
// Validate each item
for (let i = 0; i < request.orderData.items.length; i++) {
const item = request.orderData.items[i];
const fieldPrefix = `items[${i}]`;
if (!item.productId) {
errors.push(
new ValidationError(
`${fieldPrefix}.productId`,
"Product ID is required"
)
);
continue;
}
if (!item.quantity || item.quantity <= 0) {
errors.push(
new ValidationError(
`${fieldPrefix}.quantity`,
"Quantity must be greater than 0"
)
);
continue;
}
// Validate product exists and is available
const product = await this.productService.findById(item.productId);
if (!product) {
errors.push(
new ValidationError(`${fieldPrefix}.productId`, "Product not found")
);
continue;
}
if (!product.isActive) {
errors.push(
new ValidationError(
`${fieldPrefix}.productId`,
"Product is not available"
)
);
continue;
}
// Check inventory availability
const availableQuantity = await this.productService.getAvailableQuantity(
item.productId
);
if (availableQuantity < item.quantity) {
if (availableQuantity === 0) {
errors.push(
new ValidationError(
`${fieldPrefix}.quantity`,
"Product is out of stock"
)
);
} else {
warnings.push(
new ValidationWarning(
`${fieldPrefix}.quantity`,
`Only ${availableQuantity} units available, requested ${item.quantity}`
)
);
}
}
}
if (errors.length > 0) {
return new OrderValidationResponse(false, errors, warnings);
}
// Continue to next handler if item validation passes
return null;
}
}
export class OrderLimitsValidationHandler extends BaseRequestHandler<
OrderValidationRequest,
OrderValidationResponse
> {
protected async doHandle(
request: OrderValidationRequest
): Promise<OrderValidationResponse | null> {
const errors: ValidationError[] = [];
const warnings: ValidationWarning[] = [];
// Calculate order total
let orderTotal = 0;
for (const item of request.orderData.items) {
orderTotal += (item.price || 0) * (item.quantity || 0);
}
// Check minimum order value
if (orderTotal < request.validationContext.minimumOrderValue) {
errors.push(
new ValidationError(
"total",
`Order total $${orderTotal} is below minimum of $${request.validationContext.minimumOrderValue}`
)
);
}
// Check maximum order value for the customer
const customerMaxOrder =
request.customer.maxOrderValue ||
request.validationContext.defaultMaxOrderValue;
if (orderTotal > customerMaxOrder) {
errors.push(
new ValidationError(
"total",
`Order total $${orderTotal} exceeds customer limit of $${customerMaxOrder}`
)
);
}
// Check daily order limits
const todayOrderCount = await this.getTodayOrderCount(request.customer.id);
if (todayOrderCount >= request.validationContext.maxOrdersPerDay) {
errors.push(
new ValidationError(
"frequency",
`Customer has reached daily order limit of ${request.validationContext.maxOrdersPerDay}`
)
);
}
if (errors.length > 0) {
return new OrderValidationResponse(false, errors, warnings);
}
// Validation passed, continue to next handler
return null;
}
private async getTodayOrderCount(customerId: string): Promise<number> {
// Implementation would query order repository
return 0;
}
}
// Combined service using both patterns
export class OrderProcessingService {
private commandExecutor = new CommandExecutor();
private validationChain: IRequestHandler<
OrderValidationRequest,
OrderValidationResponse
>;
constructor(
private inventoryService: IInventoryService,
private paymentService: IPaymentService,
private orderRepository: IOrderRepository,
private emailService: IEmailService,
private productService: IProductService,
private customerService: ICustomerService
) {
// Set up validation chain
this.validationChain = new CustomerValidationHandler();
this.validationChain
.setNext(new OrderItemsValidationHandler(this.productService))
.setNext(new OrderLimitsValidationHandler());
}
async processOrder(orderData: CreateOrderData): Promise<Order> {
// Phase 1: Validation using Chain of Responsibility
const customer = await this.customerService.findById(orderData.customerId);
const validationRequest = new OrderValidationRequest(
orderData,
customer,
new ValidationContext({
minimumOrderValue: 10.0,
defaultMaxOrderValue: 5000.0,
maxOrdersPerDay: 10,
})
);
const validationResult = await this.validationChain.handle(
validationRequest
);
if (validationResult && !validationResult.isValid) {
throw new OrderValidationError(
"Order validation failed",
validationResult.errors
);
}
if (validationResult && validationResult.warnings.length > 0) {
console.warn("Order validation warnings:", validationResult.warnings);
}
// Phase 2: Execution using Command Pattern
const commands: ICommand[] = [
new ReserveInventoryCommand(this.inventoryService, orderData.items),
new ProcessPaymentCommand(
this.paymentService,
orderData.customerId,
orderData.total,
orderData.paymentMethod
),
new CreateOrderCommand(this.orderRepository, orderData),
new SendOrderConfirmationCommand(
this.emailService,
customer.email,
// We'll pass the order from the previous command
// This is a simplified example - in practice you'd use command results
{} as Order
),
];
try {
const executionResult = await this.commandExecutor.executeCommands(
commands
);
console.log(
`Order processed successfully in ${executionResult.totalExecutionTime}ms`
);
// Return the created order (would extract from command results)
return executionResult.commandResults[2].data as Order;
} catch (error) {
console.error("Order processing failed:", error);
throw error;
}
}
async cancelOrder(orderId: string): Promise<void> {
const order = await this.orderRepository.findById(orderId);
if (!order) {
throw new Error("Order not found");
}
if (order.status !== "confirmed") {
throw new Error("Only confirmed orders can be cancelled");
}
// Use commands for cancellation as well
const cancelCommands: ICommand[] = [
new RefundPaymentCommand(this.paymentService, order.paymentId),
new ReleaseInventoryCommand(this.inventoryService, order.items),
new MarkOrderCancelledCommand(this.orderRepository, order.id),
new SendCancellationEmailCommand(
this.emailService,
order.customerEmail,
order
),
];
await this.commandExecutor.executeCommands(cancelCommands);
}
}
// Supporting classes and interfaces
export class ValidationError {
constructor(
public field: string,
public message: string,
public code?: string
) {}
}
export class ValidationWarning {
constructor(
public field: string,
public message: string,
public code?: string
) {}
}
export class ValidationContext {
constructor(
public config: {
minimumOrderValue: number;
defaultMaxOrderValue: number;
maxOrdersPerDay: number;
}
) {}
get minimumOrderValue() {
return this.config.minimumOrderValue;
}
get defaultMaxOrderValue() {
return this.config.defaultMaxOrderValue;
}
get maxOrdersPerDay() {
return this.config.maxOrdersPerDay;
}
}
export class OrderValidationError extends Error {
constructor(message: string, public validationErrors: ValidationError[]) {
super(message);
this.name = "OrderValidationError";
}
}
// Additional command implementations for cancellation
export class RefundPaymentCommand extends BaseCommand {
constructor(
private paymentService: IPaymentService,
private paymentId: string
) {
super();
}
async doExecute(): Promise<RefundResult> {
return await this.paymentService.refund(this.paymentId);
}
async undo(): Promise<void> {
// Refunds typically cannot be undone
console.log(`Refund for payment ${this.paymentId} cannot be reversed`);
}
canUndo(): boolean {
return false;
}
getDescription(): string {
return `Refund payment ${this.paymentId}`;
}
}
// Interface definitions for clean architecture
interface IInventoryService {
reserve(productId: string, quantity: number): Promise<InventoryReservation>;
release(reservationId: string): Promise<void>;
}
interface IPaymentService {
processPayment(request: PaymentRequest): Promise<PaymentResult>;
refund(transactionId: string): Promise<RefundResult>;
}
interface IOrderRepository {
create(orderData: CreateOrderData): Promise<Order>;
findById(id: string): Promise<Order | null>;
delete(id: string): Promise<void>;
}
interface IEmailService {
sendOrderConfirmation(email: string, order: Order): Promise<EmailResult>;
}
interface IProductService {
findById(id: string): Promise<Product | null>;
getAvailableQuantity(id: string): Promise<number>;
}
interface ICustomerService {
findById(id: string): Promise<Customer>;
}
// Domain models
export class Order {
constructor(
public id: string,
public customerId: string,
public items: OrderItem[],
public total: number,
public status: string,
public paymentId: string,
public customerEmail: string
) {}
}
export class Customer {
constructor(
public id: string,
public email: string,
public isActive: boolean,
public isBlocked: boolean,
public maxOrderValue?: number
) {}
}
export class Product {
constructor(
public id: string,
public name: string,
public price: number,
public isActive: boolean
) {}
}
export class OrderItem {
constructor(
public productId: string,
public quantity: number,
public price: number
) {}
}
// Result types
interface InventoryReservation {
id: string;
productId: string;
quantity: number;
}
interface PaymentResult {
success: boolean;
transactionId: string;
errorMessage?: string;
}
interface RefundResult {
success: boolean;
refundId: string;
}
interface EmailResult {
messageId: string;
success: boolean;
}
interface CreateOrderData {
customerId: string;
items: OrderItem[];
total: number;
paymentMethod: string;
}
interface PaymentRequest {
customerId: string;
amount: number;
paymentMethod: string;
}
Key Takeaways
Advanced design patterns aren’t academic exercises—they’re the foundation that separates maintainable systems from legacy nightmares that slowly kill developer productivity.
Essential design patterns for scalable architecture:
- Repository and Unit of Work patterns provide clean data access with transaction integrity
- Dependency Injection enables testable, flexible code by removing tight coupling
- Factory and Builder patterns centralize complex object creation logic
- Observer pattern decouples event handling from business logic
- Strategy pattern makes algorithms interchangeable and testable
- Command pattern encapsulates operations with undo capabilities
- Chain of Responsibility creates flexible request processing pipelines
The architectural mindset:
- Design for change: Assume requirements will evolve and plan accordingly
- Separate concerns: Keep business logic, data access, and infrastructure separate
- Test everything: Patterns enable testing by breaking dependencies
- Think in layers: Clear boundaries between presentation, business, and data layers
- Plan for failure: Build retry, rollback, and error handling into the architecture
Implementation best practices:
- Start simple: Implement patterns when complexity justifies them, not preemptively
- Use interfaces: Program against contracts, not implementations
- Embrace composition: Prefer object composition over inheritance
- Make it testable: If you can’t easily unit test it, the design needs work
- Document decisions: Patterns solve specific problems—document why you chose each one
When to use each pattern:
- Use Repository when you need testable data access with business logic separation
- Use Dependency Injection when components need different implementations or testability
- Use Factory when object creation logic is complex or needs centralization
- Use Builder when objects have many optional parameters or complex construction
- Use Observer when multiple systems need to react to events independently
- Use Strategy when you need to switch algorithms based on runtime conditions
- Use Command when you need undo functionality or want to queue/log operations
- Use Chain of Responsibility when request processing involves multiple steps
What’s Next?
In the next blog, we’ll complete our advanced design patterns journey with CQRS (Command Query Responsibility Segregation), Event Sourcing, Saga pattern for distributed workflows, Domain-Driven Design (DDD) concepts, and Clean Architecture principles.
We’ll explore how these architectural patterns work together to create systems that can handle the most complex business domains while remaining maintainable and testable.
Because mastering basic design patterns is just the beginning. The real challenge is learning how to combine them into architectural patterns that can handle the complexity of real-world business systems—patterns that power platforms like Amazon, Netflix, and Uber.