Last active
April 7, 2025 12:06
-
-
Save sdmcraft/8748d1011b09fbc169842c0809834d9e to your computer and use it in GitHub Desktop.
Revisions
-
sdmcraft revised this gist
Apr 7, 2025 . 2 changed files with 380 additions and 190 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,190 +0,0 @@ This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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(); -
sdmcraft created this gist
Apr 7, 2025 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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.