This article explains the fundamental conflict between normalized schemas optimized for writes and denormalized views optimized for reads in high-traffic applications. It introduces CQRS (Command Query Responsibility Segregation) as the most effective architectural pattern to resolve this conflict, detailing its benefits and trade-offs compared to less optimal solutions like read replicas or schema denormalization.
Read original on Dev.to #systemdesignDatabase schema design often presents a fundamental tension: what's optimal for writes is rarely optimal for reads. A normalized schema with multiple tables, foreign keys, and ACID compliance is ideal for data integrity and efficient writes (inserts, updates), preventing duplication and anomalies. However, this structure often leads to complex, multi-table joins for read-heavy operations like dashboards and reporting, which can severely impact performance at scale.
Consider an e-commerce orders service processing 8,000 writes and 40,000 reads per minute. The write side requires a clean schema (orders, line items, payments, shipments, addresses). The read side, however, needs aggregated data for an order card or summary reports, which would involve joining these 7 tables. Basic optimizations like indexing and caching address symptoms but not the core architectural mismatch.
CQRS (Command Query Responsibility Segregation)
CQRS is an architectural pattern that separates the model used for updating information (the command side) from the model used for reading information (the query side). This allows each side to be independently optimized.
With CQRS, writes continue to target the clean, normalized, ACID-compliant PostgreSQL database. A background process then projects this data into a separate, read-optimized store. This read store is often denormalized (e.g., Elasticsearch, a separate denormalized Postgres instance, or a NoSQL database) specifically to serve dashboards and reports with minimal joins. The key trade-off is eventual consistency, meaning the read store might lag the write store by milliseconds or seconds, which is typically acceptable for analytical or display purposes where immediate consistency is not critical.
CQRS Implementation Example
Imagine an order system. When an order is placed (write), it goes into a normalized PostgreSQL database. A change data capture (CDC) mechanism or an event handler picks up this event and transforms the data into a flat, denormalized 'OrderDisplay' object. This object is then stored in an Elasticsearch index or a separate denormalized table. When a user requests to view an order (read), the query goes directly to the optimized Elasticsearch index, retrieving all necessary order details in a single, fast lookup without any complex joins on the primary database.