CQRS (Command Query Responsibility Segregation) is a software design pattern that separates read operations (queries) from write operations (commands) in an application.
"Separate the part of your system that reads data from the part that writes data."
This helps in scaling, performance, security, and maintainability, especially in complex or high-traffic systems.
🔍 Breakdown of the Term
Term | Meaning |
|---|---|
Command | Any operation that modifies data (e.g., create, update, delete) |
Query | Any operation that reads data (e.g., search, view) |
In CQRS, these two operations are handled by separate models and sometimes separate databases.
🧱 Traditional CRUD Model
One model handles:
- CREATE
- READ
- UPDATE
- DELETE🔄 CQRS Model
Command Model (Write):
- Handles state changes
- Validates business rules
- Doesn’t return data
Query Model (Read):
- Optimized for reading
- Returns data
- Doesn’t modify state<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CQRS Architecture — Try It Yourself</title>
<style>
.infographic-cqrs {
--ink: #1a1a1a;
--ink-soft: #5c5c5c;
--paper: #ffffff;
--paper-alt: #f0ece4;
--accent: #2563eb;
--accent-soft:#e3ebfd;
--line: #ddd6c9;
--good: #16a34a;
--good-soft: #dcfce7;
--warn: #b45309;
--warn-soft: #fef3c7;
--on-accent: #ffffff;
--display-font: Georgia, 'Iowan Old Style', serif;
--body-font: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--mono-font: 'SF Mono', Consolas, monospace;
background: var(--paper);
color: var(--ink);
font-family: var(--body-font);
max-width: 860px;
margin: 0 auto;
box-sizing: border-box;
overflow-x: hidden;
}
.infographic-cqrs[data-theme="dark"] {
--ink: #e8e6e1;
--ink-soft: #9a968c;
--paper: #121212;
--paper-alt: #1e1e26;
--accent: #5b8def;
--accent-soft:#202942;
--line: #2c313d;
--good: #4ade80;
--good-soft: #123420;
--warn: #fbbf24;
--warn-soft: #3a2f0e;
--on-accent: #101418;
}
.infographic-cqrs * { box-sizing: border-box; }
.cq-head { text-align: center; margin-bottom: 0.25rem; }
.cq-head h2 { font-family: var(--display-font); font-size: 1.85rem; margin: 0 0 0.5rem; }
.cq-head p { color: var(--ink-soft); font-size: 0.95rem; margin: 0 auto; max-width: 620px; line-height: 1.55; }
.cq-frame { background: var(--paper-alt); border: 1px solid var(--line); border-radius: 10px; padding: 1.25rem; margin-top: 1.5rem; }
.cq-diagram-scroll { overflow-x: auto; }
.cq-diagram-scroll svg { width: 100%; height: auto; display: block; min-width: 620px; }
.cq-shape { fill: var(--paper); stroke: var(--accent); stroke-width: 2; transition: fill 0.4s; }
.cq-shape.is-db { fill: var(--accent-soft); }
.cq-label { font-size: 12px; font-weight: 700; font-family: var(--body-font); text-anchor: middle; fill: var(--ink); }
.cq-sub { font-size: 10px; font-family: var(--mono-font); text-anchor: middle; fill: var(--ink-soft); }
.cq-version { font-size: 10px; font-family: var(--mono-font); text-anchor: middle; fill: var(--ink-soft); transition: fill 0.3s; }
.cq-version.is-stale { fill: var(--warn); font-weight: 700; }
.cq-version.is-fresh { fill: var(--good); font-weight: 700; }
.cq-edge { stroke: var(--ink-soft); stroke-width: 2; fill: none; marker-end: url(#cqArrow); opacity: 0.7; }
.cq-edge.is-flowing { stroke: var(--accent); stroke-width: 2.5; stroke-dasharray: 6 5; animation: cq-flow 0.6s linear infinite; opacity: 1; }
@keyframes cq-flow { to { stroke-dashoffset: -22; } }
.cq-eventbus { fill: var(--paper-alt); stroke: var(--accent); stroke-width: 2; stroke-dasharray: 4 3; transition: stroke 0.3s; }
.cq-eventbus.is-active { stroke-dasharray: none; }
.cq-read-ring {
fill: none; stroke: var(--good); stroke-width: 2; opacity: 0; transform-origin: 525px 195px;
}
.cq-read-ring.is-active-good { animation: cq-ring-pulse 0.75s ease-out; stroke: var(--good); }
.cq-read-ring.is-active-warn { animation: cq-ring-pulse 0.75s ease-out; stroke: var(--warn); }
@keyframes cq-ring-pulse {
0% { opacity: 0.9; transform: scale(0.55); }
100% { opacity: 0; transform: scale(1.15); }
}
.cq-edge.is-flowing-good { stroke: var(--good); stroke-width: 2.5; stroke-dasharray: 6 5; animation: cq-flow 0.6s linear infinite; opacity: 1; }
.cq-edge.is-flowing-warn { stroke: var(--warn); stroke-width: 2.5; stroke-dasharray: 6 5; animation: cq-flow 0.6s linear infinite; opacity: 1; }
.cq-run-row { display: flex; justify-content: center; gap: 0.6rem; margin-top: 1.25rem; flex-wrap: wrap; }
.cq-run-btn {
font: inherit; font-size: 0.85rem; background: var(--accent); color: var(--on-accent);
border: none; border-radius: 7px; padding: 0.6rem 1.1rem; cursor: pointer;
}
.cq-run-btn:hover { opacity: 0.9; }
.cq-run-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.cq-run-btn.is-secondary { background: var(--paper); color: var(--ink); border: 1px solid var(--line); }
.cq-msg { margin-top: 1rem; padding: 0.65rem 0.9rem; border-radius: 8px; font-size: 0.85rem; line-height: 1.55; display: none; }
.cq-msg.is-visible { display: block; }
.cq-msg.st-good { background: var(--good-soft); color: var(--good); }
.cq-msg.st-warn { background: var(--warn-soft); color: var(--warn); }
@media (prefers-reduced-motion: reduce) {
.infographic-cqrs *, .infographic-cqrs *::before, .infographic-cqrs *::after {
transition-duration: 0.001ms !important; animation-duration: 0.001ms !important;
}
}
</style>
</head>
<body>
<div class="infographic-cqrs" id="cqrs">
<div class="cq-head">
<h2>How to Sync Databases with CQRS?</h2>
<p>Commands write, queries read — but from a completely separate database. Try both and watch what happens in between.</p>
</div>
<div class="cq-frame">
<div class="cq-diagram-scroll">
<svg viewBox="0 0 720 260" id="cqSvg">
<defs>
<marker id="cqArrow" markerWidth="8" markerHeight="8" refX="4" refY="4" orient="auto">
<path d="M0,0 L8,4 L0,8 z" fill="var(--ink-soft)"></path>
</marker>
</defs>
<!-- Client -->
<circle cx="40" cy="130" r="18" fill="var(--paper)" stroke="var(--accent)" stroke-width="2"></circle>
<text class="cq-label" x="40" y="135" font-size="14">👤</text>
<text class="cq-label" x="40" y="168">Client</text>
<!-- Client -> UI -->
<path class="cq-edge" d="M60,130 L120,130"></path>
<!-- UI hexagon (simplified as rounded rect) -->
<rect class="cq-shape" x="120" y="105" width="90" height="50" rx="10"></rect>
<text class="cq-label" x="165" y="135">UI</text>
<!-- UI -> Command -->
<path class="cq-edge" d="M210,115 L280,70"></path>
<!-- UI -> Query -->
<path class="cq-edge" d="M210,145 L280,195"></path>
<!-- Command -->
<rect class="cq-shape" x="280" y="45" width="100" height="50" rx="10"></rect>
<text class="cq-label" x="330" y="75">Command</text>
<!-- Query -->
<rect class="cq-shape" x="280" y="170" width="100" height="50" rx="10"></rect>
<text class="cq-label" x="330" y="200">Query</text>
<!-- Command -> Write DB -->
<path class="cq-edge" id="cqEdgeToWrite" d="M380,70 L470,70"></path>
<!-- Query -> Read DB (reversed: read db -> query, response) -->
<path class="cq-edge" id="cqEdgeQueryResponse" d="M470,195 L380,195"></path>
<!-- Write DB -->
<rect class="cq-shape is-db" x="470" y="45" width="110" height="50" rx="8"></rect>
<text class="cq-label" x="525" y="65">Write DB</text>
<text class="cq-version" id="cqWriteVersion" x="525" y="82">v0</text>
<!-- Read DB -->
<circle class="cq-read-ring" id="cqReadRing" cx="525" cy="195" r="55"></circle>
<rect class="cq-shape is-db" x="470" y="170" width="110" height="50" rx="8"></rect>
<text class="cq-label" x="525" y="190">Read DB</text>
<text class="cq-version" id="cqReadVersion" x="525" y="207">v0</text>
<!-- Eventbus (vertical) -->
<rect class="cq-eventbus" id="cqEventbus" x="620" y="60" width="70" height="150" rx="10"></rect>
<text class="cq-label" x="655" y="140" transform="rotate(-90 655 140)">Eventbus</text>
<!-- Write DB -> Eventbus -->
<path class="cq-edge" id="cqEdgeToEventbus" d="M580,70 L620,100"></path>
<!-- Eventbus -> Read DB -->
<path class="cq-edge" id="cqEdgeToRead" d="M620,175 L580,195"></path>
<text class="cq-sub" x="655" y="30">Eventual Consistency</text>
</svg>
</div>
<div class="cq-run-row">
<button class="cq-run-btn" id="cqCommandBtn" type="button">✍️ Send Command (write)</button>
<button class="cq-run-btn is-secondary" id="cqQueryBtn" type="button">🔍 Send Query (read)</button>
<button class="cq-run-btn is-secondary" id="cqReset" type="button">↺ Reset</button>
</div>
<p class="cq-msg" id="cqMsg"></p>
</div>
</div>
<script>
(function () {
const wrapper = document.getElementById('cqrs');
const bodyAttr = document.body.getAttribute('data-darkmode');
let dark;
if (bodyAttr === 'true') {
dark = true;
} else if (bodyAttr === 'false') {
dark = false;
} else {
const match = document.cookie.match(/(?:^|;\s*)darkMode=([^;]+)/);
dark = match ? match[1] === 'true' : false;
}
if (dark) wrapper.setAttribute('data-theme', 'dark');
})();
(function () {
let writeVersion = 0;
let readVersion = 0;
const writeVersionEl = document.getElementById('cqWriteVersion');
const readVersionEl = document.getElementById('cqReadVersion');
const edgeToWrite = document.getElementById('cqEdgeToWrite');
const edgeToEventbus = document.getElementById('cqEdgeToEventbus');
const edgeToRead = document.getElementById('cqEdgeToRead');
const eventbus = document.getElementById('cqEventbus');
const commandBtn = document.getElementById('cqCommandBtn');
const queryBtn = document.getElementById('cqQueryBtn');
const msg = document.getElementById('cqMsg');
function showMsg(text, kind) {
msg.textContent = text;
msg.className = 'cq-msg is-visible st-' + kind;
}
function renderVersions() {
writeVersionEl.textContent = 'v' + writeVersion;
readVersionEl.textContent = 'v' + readVersion;
readVersionEl.className = 'cq-version' + (readVersion < writeVersion ? ' is-stale' : (readVersion > 0 ? ' is-fresh' : ''));
}
commandBtn.addEventListener('click', () => {
commandBtn.disabled = true;
writeVersion++;
renderVersions();
showMsg('✔️ Command handled — Write DB updated immediately to v' + writeVersion + '. The event now travels through the Eventbus toward Read DB. Try querying right now.', 'good');
edgeToWrite.classList.add('is-flowing');
setTimeout(() => {
edgeToWrite.classList.remove('is-flowing');
edgeToEventbus.classList.add('is-flowing');
eventbus.classList.add('is-active');
}, 500);
setTimeout(() => {
edgeToEventbus.classList.remove('is-flowing');
edgeToRead.classList.add('is-flowing');
}, 1300);
setTimeout(() => {
edgeToRead.classList.remove('is-flowing');
eventbus.classList.remove('is-active');
readVersion = writeVersion;
renderVersions();
commandBtn.disabled = false;
}, 2100);
});
queryBtn.addEventListener('click', () => {
const isStale = readVersion < writeVersion;
const ring = document.getElementById('cqReadRing');
const responseEdge = document.getElementById('cqEdgeQueryResponse');
ring.classList.remove('is-active-good', 'is-active-warn');
responseEdge.classList.remove('is-flowing-good', 'is-flowing-warn');
void ring.getBoundingClientRect();
const flavor = isStale ? 'warn' : 'good';
ring.classList.add('is-active-' + flavor);
responseEdge.classList.add('is-flowing-' + flavor);
setTimeout(() => responseEdge.classList.remove('is-flowing-' + flavor), 650);
if (isStale) {
showMsg('⚠️ Read DB answered with v' + readVersion + ', while Write DB is already at v' + writeVersion + ' — the Eventbus hasn\u2019t delivered the update yet. This gap is exactly what "eventual consistency" means.', 'warn');
} else {
showMsg('✔️ Read DB answered with v' + readVersion + ' — fully caught up with Write DB.', 'good');
}
});
document.getElementById('cqReset').addEventListener('click', () => {
writeVersion = 0;
readVersion = 0;
edgeToWrite.classList.remove('is-flowing');
edgeToEventbus.classList.remove('is-flowing');
edgeToRead.classList.remove('is-flowing');
eventbus.classList.remove('is-active');
document.getElementById('cqReadRing').classList.remove('is-active-good', 'is-active-warn');
document.getElementById('cqEdgeQueryResponse').classList.remove('is-flowing-good', 'is-flowing-warn');
commandBtn.disabled = false;
msg.classList.remove('is-visible');
renderVersions();
});
renderVersions();
})();
</script>
</body>
</html>.demo {
font-family: system-ui, sans-serif;
padding: 16px;
}
.demo h1 {
color: #2563eb;
margin: 0 0 8px;
}
.demo p {
color: #4b5563;
margin: 0;
}document.querySelector('.demo h1')?.addEventListener('click', () => {
alert('Hello from live preview!');
});✅ Benefits of CQRS
Benefit | Description |
|---|---|
🚀 Performance | Read/write operations can be optimized independently |
🔁 Scalability | You can scale read and write parts separately |
🧠 Clear separation of concerns | Cleaner and more maintainable code |
🛡️ Security and auditing | Easier to apply different permissions to reads vs writes |
🧱 Event sourcing ready | Pairs well with event sourcing architectures |
❌ Drawbacks / Challenges
Challenge | Explanation |
|---|---|
⚙️ Increased complexity | More layers, components, and code |
💾 Data consistency issues | If using separate databases, keeping them in sync can be tricky |
🔄 Eventually consistent reads | Reads might not reflect the latest writes instantly |
🧪 Harder to test/integrate | More moving parts to mock/test |
✅ When to Use CQRS
Use CQRS if:
Your system has complex business logic (e.g., banking, e-commerce)
There’s a need for high scalability
You want to optimize reads separately from writes
Your system uses event sourcing
You're dealing with many read-heavy or write-heavy operations
Don't use CQRS if:
Your app is simple (e.g., basic CRUD)
You don’t need to scale reads/writes separately
🔧 Real-World Example
🛒 E-commerce System:
Command:
PlaceOrder,CancelOrder,AddItemToCartGo through validation, inventory checks, update stock.
Query:
GetOrderHistory,GetProductDetailsRead-optimized data, may come from a denormalized view.
🔚 Summary
CQRS splits the write (command) and read (query) parts of a system.
It improves performance, scalability, and code clarity in complex systems.
But it adds infrastructure and development complexity, so it’s not ideal for simple apps.