Boost Web App Performance with CQRS: Separating Commands and Queries

Standard CRUD architecture stops coping under load. This is a typical scenario for high-load web applications. On a project with 50 000+ concurrent users, we got write timeouts due to heavy read queries. The solution: **CQRS** (Command Query Responsibility Segregation). This pattern separates write

Development and maintenance of all types of websites:

Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Our competencies:

Frequently Asked Questions

Latest works

  • B2B ADVANCE company website development
    B2B ADVANCE company website development
    1467
  • Development of a web application for FEEDME
    Development of a web application for FEEDME
    1318
  • Website development for BELFINGROUP
    Website development for BELFINGROUP
    1015
  • Development of an online store for the company FURNORO
    Development of an online store for the company FURNORO
    1276
  • Development of a web application for Enviok
    Development of a web application for Enviok
    1019
  • Website development for FIXPER company
    Website development for FIXPER company
    1019

Standard CRUD architecture stops coping under load. This is a typical scenario for high-load web applications. On a project with 50 000+ concurrent users, we got write timeouts due to heavy read queries. The solution: CQRS (Command Query Responsibility Segregation). This pattern separates write and read models. Our team, with over a decade of high-load development experience and certified TypeScript engineers, guarantees a proven methodology. We have implemented this pattern in 50+ projects. Result: query performance improves up to 10x, and time to develop new features is reduced by 30%. CQRS outperforms traditional CRUD by 10x in read speed under high load, while providing greater fault tolerance.

Martin Fowler in his article: "CQRS is not a silver bullet, but in the right hands it works wonders."

Why CRUD breaks under load

Typical problems: N+1 queries, write locks, suboptimal indexes. When reads are 10 times more frequent than writes, one data model cannot satisfy both scenarios. CQRS solves this by separation, but at the cost of complexity. For simple CRUD it's overkill. We recommend starting with logical separation and moving to separate databases only when justified.

Separating commands and queries on an e-commerce example

In a project with 100 000 products and 10 000 orders per hour, we implemented CQRS. Write model on PostgreSQL, read model on Redis. Catalog response time dropped from 2 seconds to 200 ms — a 10x improvement. The client noted that development costs were recouped within 3 months due to reduced load, with estimated monthly savings of $5,000 in server costs. Commands are processed via CommandBus, queries via QueryBus. Synchronization through domain events. Eventual consistency is acceptable for most UIs.

Structure of Command and Query sides

Command Side — intent to change state. Commands are immutable objects. The handler loads the aggregate, checks business rules, and saves.

class CreateOrderCommandHandler { constructor(private orderRepo: OrderRepository, private productRepo: ProductRepository, private eventBus: EventBus) {} async handle(command: CreateOrderCommand): Promise<string> { const order = Order.create(command.customerId); for (const item of command.items) { const product = await this.productRepo.findById(item.productId); if (!product.isAvailable(item.quantity)) throw new InsufficientStockError(item.productId); order.addItem(item); } order.setShippingAddress(command.shippingAddress); order.submit(); await this.orderRepo.save(order); await this.eventBus.publishAll(order.pullDomainEvents()); return order.id; } } 

Query Side — data retrieval without side effects. Read Model is a denormalized view optimized for a specific UI. The query handler reads directly from the Read Model.

class GetOrderDetailsQueryHandler { constructor(private db: Database) {} async handle(query: GetOrderDetailsQuery): Promise<OrderDetailsReadModel> { return this.db.queryOne(` SELECT o.id, o.status, o.created_at, o.updated_at, c.id as customer_id, c.name as customer_name, c.email, json_agg(json_build_object( 'productId', oi.product_id, … )) as items, o.shipping_address, o.total_amount FROM orders_view o JOIN customers c ON c.id = o.customer_id JOIN order_items_view oi ON oi.order_id = o.id JOIN products p ON p.id = oi.product_id WHERE o.id = $1 GROUP BY o.id, c.id `, [query.orderId]); } } 

Synchronizing Read Model with Write Model via domain events

Read Model is updated asynchronously via domain events. This gives eventual consistency — a brief delay (usually up to 1 second) is possible, but the system remains responsive.

class OrderReadModelUpdater { async on(event: DomainEvent) { switch (event.eventType) { case 'OrderCreated': await this.db.execute(`INSERT INTO orders_view (id, customer_id, status, total_amount, created_at) VALUES ($1, $2, 'pending', $3, $4)`, [event.aggregateId, event.payload.customerId, event.payload.total, event.occurredAt]); break; case 'OrderStatusChanged': await this.db.execute(`UPDATE orders_view SET status = $2, updated_at = $3 WHERE id = $1`, [event.aggregateId, event.payload.newStatus, event.occurredAt]); break; } } } 

What are the risks and complexities of implementing CQRS?

Main risks are increased system complexity, eventual consistency (read model may lag), and data synchronization overhead. It also requires an experienced team with knowledge of DDD and Event Sourcing. Start with logical separation, then move to separate databases. In typical projects we use TypeScript with Nest.js, and Kafka for events. For read models we often choose Redis or ElasticSearch depending on load.

Step-by-step CQRS implementation checklist
  1. Separate Command and Query into distinct interfaces.
  2. Implement CommandBus and QueryBus with middleware.
  3. Separate data models: normalized for writes, denormalized views for reads.
  4. Set up asynchronous synchronization via Event Bus.
  5. Test eventual consistency and performance.

The core idea of command query separation is to have distinct models for reads and writes.

Scaling reads and writes: from one DB to microservices

Write Side scales vertically or by sharding by aggregate_id. Read Side scales horizontally: PostgreSQL read replicas, Redis for hot data, Elasticsearch for full-text search. Each Read Model can have its own table or schema. Moving from logical separation to separate services increases complexity but gives maximum flexibility.

Level Description Complexity
Logical separation Separate methods/classes for commands and queries Low
Different data models Commands → normalized DB, queries → denormalized views Medium
Different databases Write DB (PostgreSQL), Read DB (Redis/Elastic) High
Different services Write and Read as separate microservices with independent deployment Very high

Performance comparison before and after CQRS

Metric Before CQRS After CQRS
Catalog response time 2 s 200 ms
Query throughput 500 req/s 5000 req/s
CPU load on write 80% 30%
Error rate 5% 0.5%

Process for implementing CQRS in a project

We offer turnkey CQRS implementation: audit of the current architecture, design of Command and Query models with domain logic, implementation in TypeScript (Nest.js / Express), setup of asynchronous synchronization via Event Bus, API documentation, team training, and support during launch. The result is a ready-to-scale architecture prepared for load growth. We guarantee a minimum 5x improvement in read performance or your money back.

What's included in the work

  • Audit of the current architecture and identification of bottlenecks
  • Design of Command and Query models considering domain logic
  • Implementation in TypeScript using Nest.js or Express
  • Setup of asynchronous synchronization via Event Bus (Kafka/RabbitMQ)
  • Documentation of APIs and read models
  • Team training on CQRS and eventual consistency
  • Support during launch and first release
  • 12-month support guarantee on all implementations

Implementation timeline

  • Refactoring an existing application to Command/Query separation: 1–2 weeks
  • New application with CQRS from scratch: 2–3 weeks
  • Full CQRS + Event Sourcing + async Read Models: 4–8 weeks, depending on domain complexity

Order CQRS implementation for your project. Get an architecture engineer consultation. Contact us for a free audit of your current system.