CQRS: Sending vs. Tracking Parcels at the Post Office

In modern applications, handling data can quickly become complicated as systems grow. Often, the code that processes changes (like creating or updating records) is mixed together with the code that simply retrieves information. This can lead to messy logic, performance problems, and difficulties when scaling. That's where CQRS, or Command Query Responsibility Segregation, comes in.

CQRS is a design pattern that splits the way you change data (commands) from the way you read data (queries). By separating these responsibilities, you make your system easier to understand, improve its performance, and allow it to scale independently. Many large-scale systems use CQRS for handling complex business logic and high-demand data operations.

Real-Life Analogy: Post Office – Sending vs. Tracking Parcels

Think about how a post office works. When you want to send a parcel, you go to a special counter, fill in a form, and hand over your package. This process changes the system – the post office now has a new parcel to deliver. But if you just want to know where your parcel is, you use the tracking service. You give them your tracking number, and they tell you the current status. Tracking a parcel only looks up information, it does not change anything in the system.

CQRS is like having two separate counters in the post office: one for sending parcels (handling changes or commands), and another for tracking parcels (retrieving information or queries). This makes the whole process clearer, faster, and more secure, since tracking never risks accidentally changing or losing your parcel.

  • Sending a parcel (Command): Changes the system by adding a new parcel to deliver.
  • Tracking a parcel (Query): Reads information about an existing parcel, but doesn't change anything.
  • Different counters: Separate staff and counters for each job, just like CQRS separates commands and queries in code.
Post office with separate counters for sending and tracking parcels
At the post office, sending a parcel changes the system, while tracking a parcel only retrieves information — just like CQRS keeps commands and queries separate.

Benefits of CQRS

Splitting your system into commands and queries brings many advantages, especially as complexity grows. By keeping responsibilities separate, you can make each part faster and safer, and scale them independently as your needs change.

  • Clearer code: Each part of your system does one thing — changes or reads — so logic is easier to follow.
  • Better performance: You can optimise queries for reading (like using caching) and commands for updating, without mixing concerns.
  • Scalability: Scale read and write operations separately based on demand.
  • Security: Reduces the risk of bugs that accidentally change data when only reading is intended.
  • Flexibility: Allows for more advanced scenarios, such as event sourcing or auditing.

What to Implement

In CQRS, you divide your codebase into two main parts: the Command Side and the Query Side. Each side may have its own data models, services, and even databases, if needed.

  • Command Side: Handles changes (create, update, delete) and may use strict validation or business logic.
  • Query Side: Handles all read-only operations and may use optimised models for fast lookups.
  • Separate models: Data structures can be different for commands and queries, which makes each side easier to maintain.
  • Optional: Separate databases: In complex systems, each side may use its own database, but this is not required for simple projects.

How It Works in C#

In a typical C# implementation, you define separate command and query handlers. Commands perform changes and queries retrieve data, each using their own logic and models. Libraries like MediatR can help organise your handlers.


// Command
public class SendParcelCommand {
    public string Recipient { get; set; }
    public string Address { get; set; }
    public string ParcelDetails { get; set; }
}

public class SendParcelHandler : IRequestHandler<SendParcelCommand> {
    public Task Handle(SendParcelCommand command) {
        // Logic to create parcel record in the system
    }
}

// Query
public class TrackParcelQuery {
    public string TrackingNumber { get; set; }
}

public class TrackParcelHandler : IRequestHandler<TrackParcelQuery, ParcelStatus> {
    public Task<ParcelStatus> Handle(TrackParcelQuery query) {
        // Logic to find and return parcel status
    }
}
  

When Should You Use It?

CQRS is ideal for systems with complex business rules, high performance requirements, or when you expect read and write loads to scale differently. It is commonly used in banking, e-commerce, and logistics platforms.

  • Your application has many users reading data at the same time as others are updating it.
  • You want to simplify and separate business logic for reads and writes.
  • You need different performance or security rules for reading and writing data.
  • You are planning to introduce event sourcing or audit trails.

When Not to Use It: For simple CRUD (Create, Read, Update, Delete) applications with low complexity, CQRS may be unnecessary and add extra overhead. Only use CQRS when the benefits clearly outweigh the added structure.

Where Is It Used in the Real World?

CQRS is used by many large organisations to handle high-demand, complex business processes. By separating how data is changed from how it is read, these companies can provide reliable and fast services even under heavy loads.

  • Banking systems: Handling millions of transactions and account lookups every day.
  • E-commerce platforms: Managing orders, inventory, and customer searches efficiently.
  • Logistics and delivery apps: Keeping order status and tracking information up to date for many users.
  • Event sourcing architectures: Recording every change to data and allowing complex business rules.

Final Thoughts

CQRS is a powerful pattern for managing complex data and high performance systems. By splitting your code for commands and queries, you make your application easier to understand, maintain, and scale. While not needed for every project, it's an essential tool for building robust, modern software.