Software Architecture

Contents

SOLID Principles

Single Responsibility Principle

// Bad Example class UserService { saveUser(user: User) { // Save user to database this.database.save(user); // Send welcome email this.emailService.sendWelcomeEmail(user); // Log activity this.logger.log(`User ${user.id} created`); } } // Good Example class UserRepository { saveUser(user: User) { return this.database.save(user); } } class EmailService { sendWelcomeEmail(user: User) { // Send welcome email } } class UserService { constructor( private userRepository: UserRepository, private emailService: EmailService, private logger: Logger ) {} async createUser(user: User) { await this.userRepository.saveUser(user); await this.emailService.sendWelcomeEmail(user); this.logger.log(`User ${user.id} created`); } }

Open/Closed Principle

// Open for extension, closed for modification interface Shape { calculateArea(): number; } class Rectangle implements Shape { constructor( private width: number, private height: number ) {} calculateArea(): number { return this.width * this.height; } } class Circle implements Shape { constructor(private radius: number) {} calculateArea(): number { return Math.PI * this.radius * this.radius; } } class AreaCalculator { calculateTotalArea(shapes: Shape[]): number { return shapes.reduce((sum, shape) => sum + shape.calculateArea(), 0); } }

Clean Architecture

Layer Separation

// Domain Layer interface User { id: string; name: string; email: string; } // Application Layer interface UserRepository { save(user: User): Promise; findById(id: string): Promise; } class CreateUserUseCase { constructor(private userRepository: UserRepository) {} async execute(userData: CreateUserDTO): Promise { const user: User = { id: generateId(), name: userData.name, email: userData.email }; await this.userRepository.save(user); return user; } } // Infrastructure Layer class PostgresUserRepository implements UserRepository { async save(user: User): Promise { // Implementation using PostgreSQL } async findById(id: string): Promise { // Implementation using PostgreSQL return null; } } // Presentation Layer class UserController { constructor(private createUser: CreateUserUseCase) {} async createUser(req: Request, res: Response) { const user = await this.createUser.execute(req.body); res.status(201).json(user); } }

Microservices

Service Communication

// Event-driven communication interface Event { id: string; type: string; data: any; timestamp: Date; } class OrderCreatedEvent implements Event { id: string; type = 'ORDER_CREATED'; timestamp: Date; constructor(public data: Order) { this.id = generateId(); this.timestamp = new Date(); } } class EventBus { private handlers: Map = new Map(); subscribe(eventType: string, handler: Function) { if (!this.handlers.has(eventType)) { this.handlers.set(eventType, []); } this.handlers.get(eventType).push(handler); } publish(event: Event) { const handlers = this.handlers.get(event.type) || []; handlers.forEach(handler => handler(event)); } } // Usage in services class OrderService { constructor(private eventBus: EventBus) {} createOrder(orderData: any) { const order = new Order(orderData); // Save order this.eventBus.publish(new OrderCreatedEvent(order)); } } class InventoryService { constructor(private eventBus: EventBus) { this.eventBus.subscribe('ORDER_CREATED', this.handleOrderCreated.bind(this)); } private handleOrderCreated(event: OrderCreatedEvent) { // Update inventory } }

Domain-Driven Design

Aggregate Example

// Domain Model class Order { private items: OrderItem[] = []; private status: OrderStatus = OrderStatus.DRAFT; constructor( readonly id: string, readonly customerId: string ) {} addItem(product: Product, quantity: number) { if (this.status !== OrderStatus.DRAFT) { throw new Error('Cannot modify confirmed order'); } const item = new OrderItem(product, quantity); this.items.push(item); } confirm() { if (this.items.length === 0) { throw new Error('Cannot confirm empty order'); } this.status = OrderStatus.CONFIRMED; } getTotalAmount(): Money { return this.items.reduce( (total, item) => total.add(item.getSubtotal()), Money.zero() ); } } class OrderItem { constructor( readonly product: Product, readonly quantity: number ) { if (quantity <= 0) { throw new Error('Quantity must be positive'); } } getSubtotal(): Money { return this.product.price.multiply(this.quantity); } } // Repository interface OrderRepository { save(order: Order): Promise; findById(id: string): Promise; findByCustomerId(customerId: string): Promise; }

CQRS Pattern

Implementation Example

// Commands interface CreateOrderCommand { customerId: string; items: Array<{ productId: string; quantity: number }>; } interface CreateOrderHandler { handle(command: CreateOrderCommand): Promise; } // Queries interface GetOrderQuery { orderId: string; } interface OrderDTO { id: string; customerId: string; items: Array<{ productId: string; quantity: number; price: number; }>; totalAmount: number; status: string; } interface GetOrderHandler { handle(query: GetOrderQuery): Promise; } // Implementation class CreateOrderCommandHandler implements CreateOrderHandler { constructor( private orderRepository: OrderRepository, private productRepository: ProductRepository ) {} async handle(command: CreateOrderCommand): Promise { const order = new Order(generateId(), command.customerId); for (const item of command.items) { const product = await this.productRepository .findById(item.productId); order.addItem(product, item.quantity); } await this.orderRepository.save(order); return order.id; } } class GetOrderQueryHandler implements GetOrderHandler { constructor(private orderRepository: OrderRepository) {} async handle(query: GetOrderQuery): Promise { const order = await this.orderRepository .findById(query.orderId); return { id: order.id, customerId: order.customerId, items: order.items.map(item => ({ productId: item.product.id, quantity: item.quantity, price: item.product.price.amount })), totalAmount: order.getTotalAmount().amount, status: order.status }; } }

Best Practices

Key Principles

  • Separation of Concerns
  • Don't Repeat Yourself (DRY)
  • Keep It Simple, Stupid (KISS)
  • You Aren't Gonna Need It (YAGNI)
  • Composition Over Inheritance
  • Law of Demeter

Architecture Decision Records

Document important architecture decisions:

  • Context
  • Decision
  • Status
  • Consequences
  • Compliance