CQRS and Event Sourcing in JavaScript: Architecture for Complex Systems

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Why CQRS and Event Sourcing Are Not "Just for the Elite"?

\n\n

When we hear the phrases "CQRS" or "Event Sourcing", many JavaScript developers imagine something from the world of enterprise architecture in Java or C#. Complex schemas, endless events, strange storage... But what if I told you that these patterns can (and should!) be used in regular Node.js applications?

\n\n

In this article, we will figure out how CQRS and Event Sourcing help solve real problems: data conflicts, complex logging, scaling, and debugging. And all of this — in pure JavaScript, without magic or complex frameworks.

\n\n

What is CQRS (Command Query Responsibility Segregation)?

\n\n

Imagine your application is a restaurant. Waiters (queries) and chefs (commands) work in the same room. Sooner or later they will start getting in each other's way. CQRS suggests separating these zones: waiters work in the hall (reading), and chefs work in the kitchen (writing).

\n\n

CQRS is the principle of separating operations into two channels:

\n
    \n
  • Commands — change the system state (Create, Update, Delete)
  • \n
  • Queries — only read data (Read)
  • \n
\n\n

In a classic REST API, we usually use the same object for reading and writing. For example, UserController contains methods GET /users/:id and POST /users. CQRS suggests separating them into different layers.

\n\n

Simple CQRS Example in JavaScript

\n\n
// Command (state change)\nclass CreateUserCommand {\n    constructor(name, email) {\n        this.name = name;\n        this.email = email;\n    }\n}\n\nclass UserCommandHandler {\n    async handle(command) {\n        // Validation, business logic, saving\n        return await db.users.insert({\n            name: command.name,\n            email: command.email,\n            created_at: new Date()\n        });\n    }\n}\n\n// Query (read only)\nclass GetUserQuery {\n    constructor(userId) {\n        this.userId = userId;\n    }\n}\n\nclass UserQueryHandler {\n    async handle(query) {\n        // Read only, no changes!\n        return await db.users.findOne({ _id: query.userId });\n    }\n}
\n\n

Does this seem to complicate the code? Yes, at first glance. But when your application grows to 50+ endpoints, separating commands and queries will save you from:

\n
    \n
  • Accidentally modifying data in GET requests
  • \n
  • Caching problems (queries are easy to cache, commands are not)
  • \n
  • Conflicts when multiple developers work in parallel
  • \n
\n\n

Event Sourcing: Remembering Every Action

\n\n

Now imagine that instead of storing the current state of a user, you store all events that have happened to them. Not "Ivan, 25 years old, Moscow", but a sequence:

\n
    \n
  1. "User created: Ivan"
  2. \n
  3. "User changed age to 25"
  4. \n
  5. "User moved to Moscow"
  6. \n
\n\n

Event Sourcing is a pattern where every state change is saved as an immutable event. The current state is computed by "replaying" all events.

\n\n

Implementing an Event Store in JavaScript

\n\n
class EventStore {\n    constructor() {\n        this.events = [];\n    }\n\n    append(aggregateId, event) {\n        this.events.push({\n            aggregateId,\n            event,\n            timestamp: new Date()\n        });\n    }\n\n    getEvents(aggregateId) {\n        return this.events.filter(e => e.aggregateId === aggregateId);\n    }\n}\n\n// Rebuilding state from events\nfunction rebuildState(events) {\n    return events.reduce((state, event) => {\n        switch(event.event.type) {\n            case 'USER_CREATED':\n                return { ...state, name: event.event.name, age: 0 };\n            case                

Blogs

Book Recommendations