Skip to content

Instantly share code, notes, and snippets.

@sdmcraft
Last active April 7, 2025 12:06
Show Gist options
  • Select an option

  • Save sdmcraft/8748d1011b09fbc169842c0809834d9e to your computer and use it in GitHub Desktop.

Select an option

Save sdmcraft/8748d1011b09fbc169842c0809834d9e to your computer and use it in GitHub Desktop.

Revisions

  1. sdmcraft revised this gist Apr 7, 2025. 2 changed files with 380 additions and 190 deletions.
    190 changes: 0 additions & 190 deletions cqrs-cdc.md
    Original file line number Diff line number Diff line change
    @@ -1,190 +0,0 @@
    # Understanding CQRS and CDC: A Practical Guide with Real-World Analogies

    ## Introduction

    In modern distributed systems, managing data operations efficiently while maintaining scalability and performance can be challenging. To understand these concepts better, let's explore them through both a practical JavaScript implementation and a familiar real-world analogy: a restaurant system.

    ## CQRS: Separating Reads and Writes

    ### What is CQRS?

    Imagine a busy restaurant that wants to improve its efficiency. Initially, the same staff takes orders AND serves food - similar to a traditional software system where the same model handles both reads and writes.

    CQRS (Command Query Responsibility Segregation) is like splitting restaurant operations into two specialized teams:

    1. **Kitchen Team (Command/Write Model)**:
    - Order takers (Command Bus)
    - Kitchen staff (Command Handlers)
    - Main kitchen (Write Database)

    2. **Service Team (Query/Read Model)**:
    - Servers checking order status (Query Service)
    - Display screens showing order status (Read Model)
    - Prep stations with prepared ingredients (Read Database)

    ### Why Use CQRS?

    1. **Optimized Performance**:
    - Restaurant: Specialized staff for each role
    - Technical: Each model optimized for its specific use case

    2. **Independent Scaling**:
    - Restaurant: Add more kitchen staff without adding servers, or vice versa
    - Technical: Scale read and write workloads separately

    3. **Specialized Models**:
    - Restaurant: Different menus for dine-in, takeout, and catering
    - Technical: Denormalized read models for specific query patterns

    ### CQRS in Action

    Let's look at both restaurant and technical implementations:

    ```javascript
    // Restaurant Order (Command)
    commandBus.publish({
    type: 'CreateOrder',
    payload: {
    id: 1,
    table: 5,
    items: ['Pasta', 'Salad'],
    specialInstructions: 'No onions'
    }
    });

    // Server Checking Status (Query)
    const orderStatus = queryService.query({
    type: 'GetOrderStatus',
    payload: { orderId: 1 }
    });
    ```

    #### The Command Path (Kitchen Operations)

    1. **Command Bus** (Order Taking System):
    ```javascript
    class CommandBus {
    publish(command) {
    // Like sending an order ticket to the kitchen
    console.log(`Command published: ${command.type}`, command.payload);
    setTimeout(() => {
    this.handlers.forEach(handler => handler.handle(command));
    }, 10);
    }
    }
    ```

    2. **Command Handler** (Kitchen Staff):
    ```javascript
    class CommandHandler {
    createOrder(orderData) {
    // Like kitchen preparing the order
    this.database.set(`order:${orderData.id}`, orderData);
    // Notify expeditor (CDC)
    this.emitCDCEvent('ORDER_CREATED', orderData);
    }
    }
    ```

    #### The Query Path (Service Operations)

    1. **Query Service** (Server Information System):
    ```javascript
    class QueryService {
    query(query) {
    // Like servers checking order status
    switch (query.type) {
    case 'GetOrderStatus':
    return this.readModel.getOrderById(query.payload.orderId);
    case 'GetAllActiveOrders':
    return this.readModel.getActiveOrders();
    }
    }
    }
    ```

    ## CDC: The Restaurant Expeditor

    ### What is CDC?

    Change Data Capture (CDC) is like the expeditor in a restaurant - a crucial role that coordinates between kitchen and service staff. The expeditor:
    - Watches for completed dishes (CDC Events)
    - Updates the service staff (Read Replica)
    - Ensures everything is synchronized

    ### CDC Implementation

    ```javascript
    // Kitchen completes a dish (CDC Event)
    class CDCProcessor {
    handleCDCEvent(cdcEvent) {
    // Like expeditor managing completed orders
    switch (cdcEvent.type) {
    case 'ORDER_CREATED':
    this.handleNewOrder(cdcEvent.data);
    break;
    case 'ORDER_UPDATED':
    this.handleOrderUpdate(cdcEvent.data);
    break;
    }
    }

    handleNewOrder(orderData) {
    // Update service stations
    this.readReplica.set(`order:${orderData.id}`, orderData);
    }
    }
    ```

    ### The Flow of Data

    Imagine a restaurant order:
    1. Customer places order (Command published)
    2. Kitchen prepares food (Command handler updates database)
    3. Expeditor notified (CDC event emitted)
    4. Service stations updated (CDC processor updates read replica)
    5. Servers can check status (Query service returns data)

    ## Real-World Scenarios

    ### Multiple Service Types

    Just as a restaurant handles different types of orders:

    ```javascript
    class ReadModel {
    getDineInOrders() {
    // Like filtering orders for dining room service
    return this.getAllOrders().filter(order => order.type === 'dine-in');
    }

    getDeliveryOrders() {
    // Like organizing delivery orders by time
    return this.getAllOrders()
    .filter(order => order.type === 'delivery')
    .sort((a, b) => a.deliveryTime - b.deliveryTime);
    }
    }
    ```

    ## When to Use CQRS and CDC

    Use these patterns when your system is like a busy restaurant that:
    - Handles different volumes of orders and status checks
    - Needs specialized views for different service types
    - Requires independent scaling of kitchen and service staff
    - Must maintain coordination between different operations

    ## Benefits of This Architecture

    1. **Scalability**: Like adding more kitchen or service staff independently
    2. **Performance**: Like optimizing kitchen and service operations separately
    3. **Flexibility**: Like having different menus for different services
    4. **Resilience**: Like kitchen continuing to work even if computer systems are down
    5. **Maintainability**: Like clear separation of kitchen and service responsibilities

    ## Conclusion

    Just as a restaurant's success depends on smooth coordination between kitchen and service staff, a CQRS system's success relies on well-orchestrated command and query operations, with CDC (the expeditor) ensuring everything stays in sync. While this pattern adds complexity, like restaurant specialization, it provides significant benefits for systems that need to handle complex, high-scale operations with different read and write requirements.

    The example code and restaurant analogy demonstrate how these concepts work together in both technical and familiar real-world contexts, making it easier to understand and implement in your own systems.
    380 changes: 380 additions & 0 deletions restaurant-cqrs-cdc.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,380 @@
    /**
    * Restaurant CQRS and CDC Example
    *
    * This file demonstrates the Command Query Responsibility Segregation (CQRS)
    * and Change Data Capture (CDC) patterns using a restaurant analogy.
    */

    // ===== Event Emitter (simulating a message bus) =====
    const EventEmitter = require('events');

    // ===== Command Bus (Write Path) =====
    class CommandBus {
    constructor() {
    this.handlers = [];
    console.log('Command Bus initialized (like the order taking system)');
    }

    subscribe(handler) {
    this.handlers.push(handler);
    console.log(`Handler subscribed to Command Bus: ${handler.constructor.name}`);
    }

    publish(command) {
    console.log(`Command published: ${command.type}`, command.payload);
    // Simulate asynchronous behavior
    setTimeout(() => {
    this.handlers.forEach(handler => handler.handle(command));
    }, 10);
    }
    }

    // ===== Command Handler (Write Path) =====
    class CommandHandler {
    constructor(database, cdcEventBus) {
    this.database = database;
    this.cdcEventBus = cdcEventBus;
    console.log('Command Handler initialized (like the kitchen staff)');
    }

    handle(command) {
    console.log(`Handling command: ${command.type}`);
    switch (command.type) {
    case 'CreateOrder':
    this.createOrder(command.payload);
    break;
    case 'UpdateOrder':
    this.updateOrder(command.payload);
    break;
    case 'CancelOrder':
    this.cancelOrder(command.payload);
    break;
    default:
    console.log(`Unknown command type: ${command.type}`);
    }
    }

    createOrder(orderData) {
    console.log(`Creating order: ${orderData.id}`);
    this.database.set(`order:${orderData.id}`, orderData);
    this.emitCDCEvent('ORDER_CREATED', orderData);
    }

    updateOrder(orderData) {
    console.log(`Updating order: ${orderData.id}`);
    this.database.set(`order:${orderData.id}`, orderData);
    this.emitCDCEvent('ORDER_UPDATED', orderData);
    }

    cancelOrder(orderData) {
    console.log(`Canceling order: ${orderData.id}`);
    this.database.delete(`order:${orderData.id}`);
    this.emitCDCEvent('ORDER_CANCELED', orderData);
    }

    emitCDCEvent(type, data) {
    console.log(`Emitting CDC event: ${type}`);
    this.cdcEventBus.emit('dataChanged', { type, data });
    }
    }

    // ===== Database (Write Path) =====
    class Database {
    constructor() {
    this.data = new Map();
    console.log('Database initialized (like the main kitchen)');
    }

    set(key, value) {
    console.log(`Setting data: ${key}`);
    this.data.set(key, value);
    }

    get(key) {
    console.log(`Getting data: ${key}`);
    return this.data.get(key);
    }

    delete(key) {
    console.log(`Deleting data: ${key}`);
    this.data.delete(key);
    }

    getAll() {
    console.log('Getting all data');
    return Array.from(this.data.entries()).map(([key, value]) => ({ key, value }));
    }
    }

    // ===== CDC Processor (Synchronization) =====
    class CDCProcessor {
    constructor(readReplica) {
    this.readReplica = readReplica;
    console.log('CDC Processor initialized (like the expeditor)');
    }

    handleCDCEvent(cdcEvent) {
    console.log(`Handling CDC event: ${cdcEvent.type}`);
    switch (cdcEvent.type) {
    case 'ORDER_CREATED':
    this.handleNewOrder(cdcEvent.data);
    break;
    case 'ORDER_UPDATED':
    this.handleOrderUpdate(cdcEvent.data);
    break;
    case 'ORDER_CANCELED':
    this.handleOrderCancel(cdcEvent.data);
    break;
    default:
    console.log(`Unknown CDC event type: ${cdcEvent.type}`);
    }
    }

    handleNewOrder(orderData) {
    console.log(`CDC: Adding new order to read replica: ${orderData.id}`);
    this.readReplica.set(`order:${orderData.id}`, orderData);
    }

    handleOrderUpdate(orderData) {
    console.log(`CDC: Updating order in read replica: ${orderData.id}`);
    this.readReplica.set(`order:${orderData.id}`, orderData);
    }

    handleOrderCancel(orderData) {
    console.log(`CDC: Removing canceled order from read replica: ${orderData.id}`);
    this.readReplica.delete(`order:${orderData.id}`);
    }
    }

    // ===== Read Model (Read Path) =====
    class ReadModel {
    constructor() {
    this.data = new Map();
    console.log('Read Model initialized (like the service stations)');
    }

    set(key, value) {
    console.log(`Setting read data: ${key}`);
    this.data.set(key, value);
    }

    get(key) {
    console.log(`Getting read data: ${key}`);
    return this.data.get(key);
    }

    delete(key) {
    console.log(`Deleting read data: ${key}`);
    this.data.delete(key);
    }

    getAll() {
    console.log('Getting all read data');
    return Array.from(this.data.entries()).map(([key, value]) => ({ key, value }));
    }

    // Specialized query methods
    getDineInOrders() {
    console.log('Getting all dine-in orders');
    return this.getAll()
    .filter(item => item.value.type === 'dine-in')
    .map(item => item.value);
    }

    getDeliveryOrders() {
    console.log('Getting all delivery orders');
    return this.getAll()
    .filter(item => item.value.type === 'delivery')
    .map(item => item.value);
    }

    getTakeoutOrders() {
    console.log('Getting all takeout orders');
    return this.getAll()
    .filter(item => item.value.type === 'takeout')
    .map(item => item.value);
    }

    getOrdersByStatus(status) {
    console.log(`Getting all orders with status: ${status}`);
    return this.getAll()
    .filter(item => item.value.status === status)
    .map(item => item.value);
    }
    }

    // ===== Query Service (Read Path) =====
    class QueryService {
    constructor(readModel) {
    this.readModel = readModel;
    console.log('Query Service initialized (like the server information system)');
    }

    query(query) {
    console.log(`Executing query: ${query.type}`);
    switch (query.type) {
    case 'GetOrderById':
    return this.readModel.get(`order:${query.payload.orderId}`);
    case 'GetAllOrders':
    return this.readModel.getAll().map(item => item.value);
    case 'GetDineInOrders':
    return this.readModel.getDineInOrders();
    case 'GetDeliveryOrders':
    return this.readModel.getDeliveryOrders();
    case 'GetTakeoutOrders':
    return this.readModel.getTakeoutOrders();
    case 'GetOrdersByStatus':
    return this.readModel.getOrdersByStatus(query.payload.status);
    default:
    console.log(`Unknown query type: ${query.type}`);
    return null;
    }
    }
    }

    // ===== Main Application =====
    function runRestaurantExample() {
    console.log('\n===== Restaurant CQRS and CDC Example =====\n');

    // Initialize components
    const commandBus = new CommandBus();
    const database = new Database();
    const cdcEventBus = new EventEmitter();
    const readReplica = new ReadModel();
    const cdcProcessor = new CDCProcessor(readReplica);
    const commandHandler = new CommandHandler(database, cdcEventBus);
    const queryService = new QueryService(readReplica);

    // Subscribe handlers
    commandBus.subscribe(commandHandler);
    cdcEventBus.on('dataChanged', cdcProcessor.handleCDCEvent.bind(cdcProcessor));

    // Example 1: Create a dine-in order
    console.log('\n----- Example 1: Create a dine-in order -----');
    commandBus.publish({
    type: 'CreateOrder',
    payload: {
    id: 1,
    type: 'dine-in',
    table: 5,
    items: ['Pasta', 'Salad'],
    specialInstructions: 'No onions',
    status: 'pending',
    timestamp: new Date().toISOString()
    }
    });

    // Example 2: Create a delivery order
    console.log('\n----- Example 2: Create a delivery order -----');
    commandBus.publish({
    type: 'CreateOrder',
    payload: {
    id: 2,
    type: 'delivery',
    address: '123 Main St',
    items: ['Pizza', 'Wings'],
    specialInstructions: 'Extra cheese',
    status: 'pending',
    deliveryTime: new Date(Date.now() + 3600000).toISOString(),
    timestamp: new Date().toISOString()
    }
    });

    // Example 3: Create a takeout order
    console.log('\n----- Example 3: Create a takeout order -----');
    commandBus.publish({
    type: 'CreateOrder',
    payload: {
    id: 3,
    type: 'takeout',
    items: ['Burger', 'Fries'],
    specialInstructions: 'No pickles',
    status: 'pending',
    pickupTime: new Date(Date.now() + 1800000).toISOString(),
    timestamp: new Date().toISOString()
    }
    });

    // Wait for commands to be processed
    setTimeout(() => {
    // Example 4: Update an order status
    console.log('\n----- Example 4: Update an order status -----');
    commandBus.publish({
    type: 'UpdateOrder',
    payload: {
    id: 1,
    type: 'dine-in',
    table: 5,
    items: ['Pasta', 'Salad'],
    specialInstructions: 'No onions',
    status: 'preparing',
    timestamp: new Date().toISOString()
    }
    });

    // Wait for commands to be processed
    setTimeout(() => {
    // Example 5: Query all orders
    console.log('\n----- Example 5: Query all orders -----');
    const allOrders = queryService.query({ type: 'GetAllOrders' });
    console.log('All orders:', allOrders);

    // Example 6: Query dine-in orders
    console.log('\n----- Example 6: Query dine-in orders -----');
    const dineInOrders = queryService.query({ type: 'GetDineInOrders' });
    console.log('Dine-in orders:', dineInOrders);

    // Example 7: Query delivery orders
    console.log('\n----- Example 7: Query delivery orders -----');
    const deliveryOrders = queryService.query({ type: 'GetDeliveryOrders' });
    console.log('Delivery orders:', deliveryOrders);

    // Example 8: Query takeout orders
    console.log('\n----- Example 8: Query takeout orders -----');
    const takeoutOrders = queryService.query({ type: 'GetTakeoutOrders' });
    console.log('Takeout orders:', takeoutOrders);

    // Example 9: Query orders by status
    console.log('\n----- Example 9: Query orders by status -----');
    const pendingOrders = queryService.query({
    type: 'GetOrdersByStatus',
    payload: { status: 'pending' }
    });
    console.log('Pending orders:', pendingOrders);

    const preparingOrders = queryService.query({
    type: 'GetOrdersByStatus',
    payload: { status: 'preparing' }
    });
    console.log('Preparing orders:', preparingOrders);

    // Example 10: Cancel an order
    console.log('\n----- Example 10: Cancel an order -----');
    commandBus.publish({
    type: 'CancelOrder',
    payload: {
    id: 2,
    type: 'delivery',
    status: 'canceled',
    timestamp: new Date().toISOString()
    }
    });

    // Wait for commands to be processed
    setTimeout(() => {
    // Example 11: Final state of the system
    console.log('\n----- Example 11: Final state of the system -----');
    console.log('Main database state:');
    console.log(database.getAll());

    console.log('\nRead replica state:');
    console.log(readReplica.getAll());

    console.log('\n===== Restaurant CQRS and CDC Example Complete =====\n');
    }, 100);
    }, 100);
    }, 100);
    }

    // Run the example
    runRestaurantExample();
  2. sdmcraft created this gist Apr 7, 2025.
    190 changes: 190 additions & 0 deletions cqrs-cdc.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,190 @@
    # Understanding CQRS and CDC: A Practical Guide with Real-World Analogies

    ## Introduction

    In modern distributed systems, managing data operations efficiently while maintaining scalability and performance can be challenging. To understand these concepts better, let's explore them through both a practical JavaScript implementation and a familiar real-world analogy: a restaurant system.

    ## CQRS: Separating Reads and Writes

    ### What is CQRS?

    Imagine a busy restaurant that wants to improve its efficiency. Initially, the same staff takes orders AND serves food - similar to a traditional software system where the same model handles both reads and writes.

    CQRS (Command Query Responsibility Segregation) is like splitting restaurant operations into two specialized teams:

    1. **Kitchen Team (Command/Write Model)**:
    - Order takers (Command Bus)
    - Kitchen staff (Command Handlers)
    - Main kitchen (Write Database)

    2. **Service Team (Query/Read Model)**:
    - Servers checking order status (Query Service)
    - Display screens showing order status (Read Model)
    - Prep stations with prepared ingredients (Read Database)

    ### Why Use CQRS?

    1. **Optimized Performance**:
    - Restaurant: Specialized staff for each role
    - Technical: Each model optimized for its specific use case

    2. **Independent Scaling**:
    - Restaurant: Add more kitchen staff without adding servers, or vice versa
    - Technical: Scale read and write workloads separately

    3. **Specialized Models**:
    - Restaurant: Different menus for dine-in, takeout, and catering
    - Technical: Denormalized read models for specific query patterns

    ### CQRS in Action

    Let's look at both restaurant and technical implementations:

    ```javascript
    // Restaurant Order (Command)
    commandBus.publish({
    type: 'CreateOrder',
    payload: {
    id: 1,
    table: 5,
    items: ['Pasta', 'Salad'],
    specialInstructions: 'No onions'
    }
    });

    // Server Checking Status (Query)
    const orderStatus = queryService.query({
    type: 'GetOrderStatus',
    payload: { orderId: 1 }
    });
    ```

    #### The Command Path (Kitchen Operations)

    1. **Command Bus** (Order Taking System):
    ```javascript
    class CommandBus {
    publish(command) {
    // Like sending an order ticket to the kitchen
    console.log(`Command published: ${command.type}`, command.payload);
    setTimeout(() => {
    this.handlers.forEach(handler => handler.handle(command));
    }, 10);
    }
    }
    ```

    2. **Command Handler** (Kitchen Staff):
    ```javascript
    class CommandHandler {
    createOrder(orderData) {
    // Like kitchen preparing the order
    this.database.set(`order:${orderData.id}`, orderData);
    // Notify expeditor (CDC)
    this.emitCDCEvent('ORDER_CREATED', orderData);
    }
    }
    ```

    #### The Query Path (Service Operations)

    1. **Query Service** (Server Information System):
    ```javascript
    class QueryService {
    query(query) {
    // Like servers checking order status
    switch (query.type) {
    case 'GetOrderStatus':
    return this.readModel.getOrderById(query.payload.orderId);
    case 'GetAllActiveOrders':
    return this.readModel.getActiveOrders();
    }
    }
    }
    ```

    ## CDC: The Restaurant Expeditor

    ### What is CDC?

    Change Data Capture (CDC) is like the expeditor in a restaurant - a crucial role that coordinates between kitchen and service staff. The expeditor:
    - Watches for completed dishes (CDC Events)
    - Updates the service staff (Read Replica)
    - Ensures everything is synchronized

    ### CDC Implementation

    ```javascript
    // Kitchen completes a dish (CDC Event)
    class CDCProcessor {
    handleCDCEvent(cdcEvent) {
    // Like expeditor managing completed orders
    switch (cdcEvent.type) {
    case 'ORDER_CREATED':
    this.handleNewOrder(cdcEvent.data);
    break;
    case 'ORDER_UPDATED':
    this.handleOrderUpdate(cdcEvent.data);
    break;
    }
    }

    handleNewOrder(orderData) {
    // Update service stations
    this.readReplica.set(`order:${orderData.id}`, orderData);
    }
    }
    ```

    ### The Flow of Data

    Imagine a restaurant order:
    1. Customer places order (Command published)
    2. Kitchen prepares food (Command handler updates database)
    3. Expeditor notified (CDC event emitted)
    4. Service stations updated (CDC processor updates read replica)
    5. Servers can check status (Query service returns data)

    ## Real-World Scenarios

    ### Multiple Service Types

    Just as a restaurant handles different types of orders:

    ```javascript
    class ReadModel {
    getDineInOrders() {
    // Like filtering orders for dining room service
    return this.getAllOrders().filter(order => order.type === 'dine-in');
    }

    getDeliveryOrders() {
    // Like organizing delivery orders by time
    return this.getAllOrders()
    .filter(order => order.type === 'delivery')
    .sort((a, b) => a.deliveryTime - b.deliveryTime);
    }
    }
    ```

    ## When to Use CQRS and CDC

    Use these patterns when your system is like a busy restaurant that:
    - Handles different volumes of orders and status checks
    - Needs specialized views for different service types
    - Requires independent scaling of kitchen and service staff
    - Must maintain coordination between different operations

    ## Benefits of This Architecture

    1. **Scalability**: Like adding more kitchen or service staff independently
    2. **Performance**: Like optimizing kitchen and service operations separately
    3. **Flexibility**: Like having different menus for different services
    4. **Resilience**: Like kitchen continuing to work even if computer systems are down
    5. **Maintainability**: Like clear separation of kitchen and service responsibilities

    ## Conclusion

    Just as a restaurant's success depends on smooth coordination between kitchen and service staff, a CQRS system's success relies on well-orchestrated command and query operations, with CDC (the expeditor) ensuring everything stays in sync. While this pattern adds complexity, like restaurant specialization, it provides significant benefits for systems that need to handle complex, high-scale operations with different read and write requirements.

    The example code and restaurant analogy demonstrate how these concepts work together in both technical and familiar real-world contexts, making it easier to understand and implement in your own systems.