Isaac.

Command/Query Separation (CQRS)

Introduction

Command/Query Responsibility Segregation (CQRS) is an architectural pattern that separates read operations (queries) from write operations (commands). This separation can improve scalability, maintainability, and performance, particularly in complex systems with high transaction volumes.

Why CQRS?

  • Separates read and write concerns for better clarity.
  • Enables independent optimization of read and write paths.
  • Supports eventual consistency in distributed systems.
  • Helps prevent accidental data corruption by enforcing clear boundaries.

Ignoring CQRS in systems that require clear separation of commands and queries can lead to convoluted services, hard-to-maintain code, and performance bottlenecks.

Examples Across Platforms

ASP.NET Core (Primary Focus)

Command and Query separation using MediatR:

// Command
public class CreateUserCommand : IRequest<Guid>
{
    public string Name { get; set; }
}
// Command Handler
public class CreateUserCommandHandler : IRequestHandler<CreateUserCommand, Guid>
{
    private readonly AppDbContext _context;
    public CreateUserCommandHandler(AppDbContext context) { _context = context; }

    public async Task<Guid> Handle(CreateUserCommand request, CancellationToken cancellationToken)
    {
        var user = new User { Name = request.Name };
        _context.Users.Add(user);
        await _context.SaveChangesAsync(cancellationToken);
        return user.Id;
    }
}
// Query
public class GetUserByIdQuery : IRequest<User>
{
    public Guid Id { get; set; }
}
// Query Handler
public class GetUserByIdQueryHandler : IRequestHandler<GetUserByIdQuery, User>
{
    private readonly AppDbContext _context;
    public GetUserByIdQueryHandler(AppDbContext context) { _context = context; }

    public async Task<User> Handle(GetUserByIdQuery request, CancellationToken cancellationToken)
    {
        return await _context.Users.FindAsync(request.Id);
    }
}
Spring Boot

CQRS example using Spring services:

// Command Service
@Service
public class CreateUserService {
    @Autowired
    private UserRepository repository;
    
    public User execute(String name) {
        User user = new User(name);
        return repository.save(user);
    }
}
// Query Service
@Service
public class GetUserService {
    @Autowired
    private UserRepository repository;
    
    public Optional<User> execute(Long id) {
        return repository.findById(id);
    }
}
Express.js

CQRS example using separate controllers:

// Command: create user
app.post('/users', (req, res) => {
  const user = { id: uuid(), name: req.body.name };
  db.users.push(user);
  res.status(201).json(user);
});
// Query: get user by ID
app.get('/users/:id', (req, res) => {
  const user = db.users.find(u => u.id === req.params.id);
  res.json(user);
});
Next.js

CQRS example in API routes:

// pages/api/users/index.ts (Command)
export default function handler(req, res) {
  if(req.method === 'POST'){
    const user = { id: crypto.randomUUID(), name: req.body.name };
    users.push(user);
    res.status(201).json(user);
  }
}
// pages/api/users/[id].ts (Query)
export default function handler(req, res) {
  const user = users.find(u => u.id === req.query.id);
  res.status(200).json(user);
}
Flask

CQRS with separate endpoints:

from flask import Flask, request, jsonify
app = Flask(__name__)
users = []

# Command
@app.route('/users', methods=['POST'])
def create_user():
    user = {'id': len(users)+1, 'name': request.json['name']}
    users.append(user)
    return jsonify(user), 201

# Query
@app.route('/users/<int:user_id>', methods=['GET'])
def get_user(user_id):
    user = next((u for u in users if u['id'] == user_id), None)
    return jsonify(user)
Laravel

CQRS with separate command and query services:

// app/Services/Commands/CreateUser.php
namespace App\Services\Commands;
use App\Models\User;

class CreateUser {
    public function execute($name) {
        return User::create(['name' => $name]);
    }
}
// app/Services/Queries/GetUserById.php
namespace App\Services\Queries;
use App\Models\User;

class GetUserById {
    public function execute($id) {
        return User::find($id);
    }
}

Conclusion

CQRS is a powerful architectural pattern that separates commands and queries to improve system clarity, maintainability, and performance. By separating read and write responsibilities, developers can optimize each path independently and enforce stricter domain rules.

Key Takeaways:

  • Commands modify state, queries read state.
  • Separation simplifies code and enforces boundaries.
  • ASP.NET offers tools like MediatR for easy implementation.
  • Ignoring CQRS in complex systems can lead to messy, hard-to-maintain code and performance issues.