Multi-Primary Replication is a database replication strategy where multiple database nodes (servers) can accept write operations.
π Unlike traditional Primary-Secondary (single-writer) setups, here each node acts as a master, and data changes are replicated to all other nodes.
β Why Use Multi-Primary?
High Availability: If one node fails, others can still accept writes.
Low Latency: Writes can be directed to the nearest node (e.g., in different regions).
Load Balancing: Write and read loads can be spread across servers.
π How It Works
Each master node:
Accepts write operations independently
Replicates those changes to other masters
Tries to maintain consistency across all nodes
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Multi-Primary Replication β Try It Yourself</title>
<style>
.infographic-multimaster {
--ink: #1a1a1a;
--ink-soft: #5c5c5c;
--paper: #ffffff;
--paper-alt: #f0ece4;
--accent: #2563eb;
--accent-soft:#e3ebfd;
--line: #ddd6c9;
--good: #16a34a;
--good-soft: #dcfce7;
--bad: #dc2626;
--bad-soft: #fee2e2;
--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: 880px;
margin: 0 auto;
box-sizing: border-box;
overflow-x: hidden;
}
.infographic-multimaster[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;
--bad: #f87171;
--bad-soft: #3a1414;
--on-accent: #101418;
}
.infographic-multimaster * { box-sizing: border-box; }
.mm-head { text-align: center; margin-bottom: 0.25rem; }
.mm-head h2 { font-family: var(--display-font); font-size: 1.85rem; margin: 0 0 0.5rem; }
.mm-head p { color: var(--ink-soft); font-size: 0.95rem; margin: 0 auto; max-width: 640px; line-height: 1.55; }
.mm-frame { background: var(--paper-alt); border: 1px solid var(--line); border-radius: 10px; padding: 1.25rem; margin-top: 1.5rem; }
.mm-diagram-scroll { overflow-x: auto; }
.mm-diagram-scroll svg { width: 100%; height: auto; display: block; min-width: 560px; }
.mm-shape { fill: var(--accent-soft); stroke: var(--accent); stroke-width: 2; transition: fill 0.4s, stroke 0.4s; }
.mm-shape.is-master { fill: var(--accent); }
.mm-shape.is-conflict { fill: var(--bad-soft); stroke: var(--bad); }
.mm-label { font-size: 12px; font-weight: 700; font-family: var(--body-font); text-anchor: middle; fill: var(--ink); }
.mm-label.is-onaccent { fill: var(--on-accent); }
.mm-value { font-size: 11px; font-family: var(--mono-font); text-anchor: middle; fill: var(--ink-soft); transition: fill 0.3s; }
.mm-value.is-onaccent { fill: rgba(255,255,255,0.9); }
.mm-value.is-conflict { fill: var(--bad); font-weight: 700; }
.mm-edge { stroke: var(--ink-soft); stroke-width: 2; fill: none; marker-end: url(#mmArrow); opacity: 0.6; }
.mm-edge.is-flowing { stroke: var(--accent); stroke-width: 2.5; stroke-dasharray: 6 5; animation: mm-flow 0.6s linear infinite; opacity: 1; }
.mm-edge.is-flowing-bad { stroke: var(--bad); stroke-width: 2.5; stroke-dasharray: 6 5; animation: mm-flow 0.6s linear infinite; opacity: 1; }
@keyframes mm-flow { to { stroke-dashoffset: -22; } }
.mm-run-row { display: flex; justify-content: center; gap: 0.6rem; margin-top: 1.25rem; flex-wrap: wrap; }
.mm-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;
}
.mm-run-btn:hover { opacity: 0.9; }
.mm-run-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.mm-run-btn.is-danger { background: var(--bad); }
.mm-run-btn.is-secondary { background: var(--paper); color: var(--ink); border: 1px solid var(--line); }
.mm-msg { margin-top: 1rem; padding: 0.65rem 0.9rem; border-radius: 8px; font-size: 0.85rem; line-height: 1.55; display: none; }
.mm-msg.is-visible { display: block; }
.mm-msg.st-good { background: var(--good-soft); color: var(--good); }
.mm-msg.st-bad { background: var(--bad-soft); color: var(--bad); }
@media (prefers-reduced-motion: reduce) {
.infographic-multimaster *, .infographic-multimaster *::before, .infographic-multimaster *::after {
transition-duration: 0.001ms !important; animation-duration: 0.001ms !important;
}
}
</style>
</head>
<body>
<div class="infographic-multimaster" id="multimaster">
<div class="mm-head">
<h2>Multi-Primary Replication</h2>
<p>Both masters can accept writes. Try writing to each one separately β then try writing to both at nearly the same time, and see the problem single-master replication never has to deal with.</p>
</div>
<div class="mm-frame">
<div class="mm-diagram-scroll">
<svg viewBox="0 0 640 300" id="mmSvg">
<defs>
<marker id="mmArrow" 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>
<!-- Replica 1 (top-left) -->
<rect class="mm-shape" x="20" y="20" width="100" height="55" rx="8"></rect>
<text class="mm-label" x="70" y="45">Replica 1</text>
<text class="mm-value" id="mmR1" x="70" y="63">β</text>
<path class="mm-edge" id="mmEdgeR1" d="M150,105 L100,80"></path>
<!-- Replica 2 (bottom-left) -->
<rect class="mm-shape" x="20" y="225" width="100" height="55" rx="8"></rect>
<text class="mm-label" x="70" y="250">Replica 2</text>
<text class="mm-value" id="mmR2" x="70" y="268">β</text>
<path class="mm-edge" id="mmEdgeR2" d="M150,195 L100,225"></path>
<!-- Master 1 -->
<rect class="mm-shape is-master" id="mmM1Box" x="150" y="105" width="120" height="90" rx="10"></rect>
<text class="mm-label is-onaccent" x="210" y="140">Master 1</text>
<text class="mm-value is-onaccent" id="mmM1" x="210" y="160">β</text>
<!-- Master 1 <-> Master 2 -->
<path class="mm-edge" id="mmEdgeM1toM2" d="M270,130 L370,130"></path>
<path class="mm-edge" id="mmEdgeM2toM1" d="M370,170 L270,170"></path>
<text class="mm-value" x="320" y="118">sync</text>
<!-- Master 2 -->
<rect class="mm-shape is-master" id="mmM2Box" x="370" y="105" width="120" height="90" rx="10"></rect>
<text class="mm-label is-onaccent" x="430" y="140">Master 2</text>
<text class="mm-value is-onaccent" id="mmM2" x="430" y="160">β</text>
<!-- Replica 3 (top-right) -->
<rect class="mm-shape" x="520" y="20" width="100" height="55" rx="8"></rect>
<text class="mm-label" x="570" y="45">Replica 3</text>
<text class="mm-value" id="mmR3" x="570" y="63">β</text>
<path class="mm-edge" id="mmEdgeR3" d="M490,105 L540,80"></path>
<!-- Replica 4 (bottom-right) -->
<rect class="mm-shape" x="520" y="225" width="100" height="55" rx="8"></rect>
<text class="mm-label" x="570" y="250">Replica 4</text>
<text class="mm-value" id="mmR4" x="570" y="268">β</text>
<path class="mm-edge" id="mmEdgeR4" d="M490,195 L540,225"></path>
</svg>
</div>
<div class="mm-run-row">
<button class="mm-run-btn" id="writeM1Btn" type="button">βοΈ Write to Master 1</button>
<button class="mm-run-btn" id="writeM2Btn" type="button">βοΈ Write to Master 2</button>
<button class="mm-run-btn is-danger" id="conflictBtn" type="button">π₯ Write to both at once (conflict)</button>
<button class="mm-run-btn is-secondary" id="mmReset" type="button">βΊ Reset</button>
</div>
<p class="mm-msg" id="mmMsg"></p>
</div>
</div>
<script>
(function () {
const wrapper = document.getElementById('multimaster');
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 letterCode = 65; // 'A'
const els = {
m1: document.getElementById('mmM1'), m2: document.getElementById('mmM2'),
r1: document.getElementById('mmR1'), r2: document.getElementById('mmR2'),
r3: document.getElementById('mmR3'), r4: document.getElementById('mmR4')
};
const boxes = { m1: document.getElementById('mmM1Box'), m2: document.getElementById('mmM2Box') };
const edges = {
r1: document.getElementById('mmEdgeR1'), r2: document.getElementById('mmEdgeR2'),
r3: document.getElementById('mmEdgeR3'), r4: document.getElementById('mmEdgeR4'),
m1to2: document.getElementById('mmEdgeM1toM2'), m2to1: document.getElementById('mmEdgeM2toM1')
};
const writeM1Btn = document.getElementById('writeM1Btn');
const writeM2Btn = document.getElementById('writeM2Btn');
const conflictBtn = document.getElementById('conflictBtn');
const msg = document.getElementById('mmMsg');
function nextLetter() {
const l = String.fromCharCode(letterCode);
letterCode++;
return l;
}
function showMsg(text, kind) {
msg.textContent = text;
msg.className = 'mm-msg is-visible st-' + kind;
}
function setVal(key, v) { els[key].textContent = v; }
function flash(edge) {
edge.classList.add('is-flowing');
setTimeout(() => edge.classList.remove('is-flowing'), 700);
}
function disableAll(state) {
writeM1Btn.disabled = state;
writeM2Btn.disabled = state;
conflictBtn.disabled = state;
}
writeM1Btn.addEventListener('click', () => {
disableAll(true);
const v = nextLetter();
setVal('m1', v);
showMsg('βοΈ Master 1 wrote "' + v + '" locally, immediately. Now replicating to Replica 1, Replica 2, and Master 2...', 'good');
flash(edges.r1); flash(edges.r2); flash(edges.m1to2);
setTimeout(() => { setVal('r1', v); setVal('r2', v); }, 650);
setTimeout(() => {
setVal('m2', v);
showMsg('βοΈ Master 2 received "' + v + '" from Master 1 and is now in sync β both masters and all four replicas agree.', 'good');
disableAll(false);
}, 900);
});
writeM2Btn.addEventListener('click', () => {
disableAll(true);
const v = nextLetter();
setVal('m2', v);
showMsg('βοΈ Master 2 wrote "' + v + '" locally, immediately. Now replicating to Replica 3, Replica 4, and Master 1...', 'good');
flash(edges.r3); flash(edges.r4); flash(edges.m2to1);
setTimeout(() => { setVal('r3', v); setVal('r4', v); }, 650);
setTimeout(() => {
setVal('m1', v);
showMsg('βοΈ Master 1 received "' + v + '" from Master 2 and is now in sync β both masters and all four replicas agree.', 'good');
disableAll(false);
}, 900);
});
conflictBtn.addEventListener('click', () => {
disableAll(true);
const vA = nextLetter();
const vB = nextLetter();
setVal('m1', vA); setVal('m2', vB);
showMsg('β οΈ Both masters were written to at nearly the same instant β Master 1 got "' + vA + '", Master 2 got "' + vB + '" β before either one heard about the other\u2019s change.', 'bad');
flash(edges.r1); flash(edges.r2); flash(edges.r3); flash(edges.r4);
setTimeout(() => { setVal('r1', vA); setVal('r2', vA); setVal('r3', vB); setVal('r4', vB); }, 650);
setTimeout(() => {
edges.m1to2.classList.add('is-flowing-bad');
edges.m2to1.classList.add('is-flowing-bad');
setTimeout(() => { edges.m1to2.classList.remove('is-flowing-bad'); edges.m2to1.classList.remove('is-flowing-bad'); }, 700);
}, 900);
setTimeout(() => {
boxes.m1.classList.add('is-conflict');
boxes.m2.classList.add('is-conflict');
els.m1.classList.add('is-conflict');
els.m2.classList.add('is-conflict');
showMsg('β CONFLICT: when the masters finally sync, each one has a change the other doesn\u2019t recognize as "latest" β Master 1 has "' + vA + '" locally but is told about "' + vB + '", and vice versa. Something has to decide the winner (e.g. last-write-wins by timestamp, or manual merge) β this is the real cost of letting two masters accept writes at once.', 'bad');
disableAll(false);
}, 1600);
});
document.getElementById('mmReset').addEventListener('click', () => {
letterCode = 65;
Object.keys(els).forEach(k => { setVal(k, 'β'); els[k].classList.remove('is-conflict'); });
boxes.m1.classList.remove('is-conflict');
boxes.m2.classList.remove('is-conflict');
Object.values(edges).forEach(e => { e.classList.remove('is-flowing', 'is-flowing-bad'); });
disableAll(false);
msg.classList.remove('is-visible');
});
})();
</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!');
});π§ Example Systems Supporting Multi-Primary
Database | Notes |
|---|---|
πΉ MySQL (Group Replication) | Supports multi-primary mode with conflict handling |
πΉ CouchDB | Peer-to-peer replication with conflict resolution |
πΉ Cassandra | All nodes can accept writes (eventually consistent) |
πΉ Active Directory | Uses multi-master replication |
πΉ CockroachDB | Globally distributed SQL with multi-master writes |
β οΈ Challenges in Multi-Primary
Challenge | Description |
|---|---|
β Conflict Resolution | Two users writing to the same record at the same time in different nodes β what wins? |
βοΈ Complex Sync Logic | Requires conflict detection and resolution policies |
π Consistency Issues | Risk of stale reads or out-of-sync nodes |
β±οΈ Latency in sync | Writes need to be propagated to all nodes, which can slow down performance if not optimized |
π Network Partitions | Can lead to split-brain (inconsistent data across nodes) |
β When to Use Multi-Primary
Use it when:
You have geo-distributed apps needing fast local writes
You want high availability with no single point of failure
You are okay with eventual consistency or can handle conflicts
Avoid if:
Your app needs strong consistency (e.g., financial transactions)
Conflict resolution logic would be complex or risky
π Summary
Feature | Multi-Primary Replication |
|---|---|
Writes Allowed on | Multiple nodes |
Read/Write Scalability | High |
Conflict Risk | Yes (must be handled) |
Use Case | Distributed apps, high availability |
Drawback | Conflict resolution, complexity |