Why Regular CRUD Applications Stop Working When Business Gets Complex
\n\nHave you ever encountered a situation where a simple task — "show the order change history" — turns into a nightmare? You dig into the database, look at logs, try to reconstruct who changed the status and when. And then a request comes in: "Create an audit of all user actions for the last three years." And you realize — the current architecture won't handle it.
\n\nHi, my name is PythonLib, and today we'll break down two powerful patterns that save the day in such situations: CQRS (Command Query Responsibility Segregation) and Event Sourcing. We'll write code in TypeScript that you can use immediately in a real project.
\n\nIf you develop fintech, marketplaces, CRM, or any systems where history and audit matter — this article is for you.
\n\nWhat is CQRS in Simple Terms
\n\nImagine a library. Usually, one librarian both checks out books, accepts returns, and keeps records. But when there are many visitors — a queue forms. CQRS proposes separating responsibilities: one librarian only checks out (commands), the second only accepts and records (queries).
\n\nIn programming, this means separating models for reading and writing. Instead of one monolithic repository, we create:
\n\n- \n
- Command Model — for changing data (Create, Update, Delete) \n
- Query Model — for reading data (Read) \n
Seems like overcomplication? Yes, at the start. But when the system grows, you gain flexibility: you can optimize reading separately from writing, use different databases, caching.
\n\nTypeScript Example: Simple CQRS without Event Sourcing
\n\nLet's say we have an order management system. First, let's define the entities:
\n\n// domain/order.ts\nexport interface Order {\n id: string;\n userId: string;\n items: OrderItem[];\n status: 'pending' | 'confirmed' | 'shipped' | 'cancelled';\n total: number;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface OrderItem {\n productId: string;\n quantity: number;\n price: number;\n}\n\n\nNow let's create a command and a query:
\n\n// application/commands/create-order.command.ts\nexport class CreateOrderCommand {\n constructor(\n public readonly userId: string,\n public readonly items: OrderItem[]\n ) {}\n}\n\n// application/commands/create-order.handler.ts\nimport { CommandHandler, ICommandHandler } from '@nestjs/cqrs';\nimport { OrderRepository } from '../repositories/order.repository';\n\n@CommandHandler(CreateOrderCommand)\nexport class CreateOrderHandler implements ICommandHandler<CreateOrderCommand> {\n constructor(private readonly orderRepo: OrderRepository) {}\n\n async execute(command: CreateOrderCommand): Promise<string> {\n const order: Order = {\n id: uuid(),\n userId: command.userId,\n items: command.items,\n status: 'pending',\n total: command.items.reduce((sum, item) => sum + item.price * item.quantity, 0),\n createdAt: new Date(),\n updatedAt: new Date()\n };\n await this.orderRepo.save(order);\n return order.id;\n }\n}\n\n\nAnd for reading — a separate handler:
\n\n// application/queries/get-order.query.ts\nexport class GetOrderQuery {\n constructor(public readonly orderId: string) {}\n}\n\n// application/queries/get-order.handler.ts\nimport { QueryHandler, IQueryHandler } from '@nestjs/cqrs';\nimport { OrderReadRepository } from '../repositories/order-read.repository';\n\n@QueryHandler(GetOrderQuery)\nexport class GetOrderHandler implements IQueryHandler<GetOrderQuery> {\n constructor(private readonly readRepo: OrderReadRepository) {}\n\n async execute(query: GetOrderQuery): Promise<Order | null> {\n return this.readRepo.findById(query.orderId);\n }\n}\n