Graph Database Africa · When One Thing Breaks · live demos

Graph Lab

The three live demos from When One Thing Breaks, with you in control. They play on their own; click or use the arrow keys to take over.

click or → next step ← previous Autoplay resumes after 30 seconds idle

Click inside the demo once so it receives your arrow keys. The platform in the demos is fictional.

Run the demo yourself in Neo4j Cypher · Aura Free

Paste into the Aura query editor one block at a time. Expected results are in the comments. Download .cypher

// =====================================================================
// When One Thing Breaks: dependency graphs for SRE
// Runnable demo script for Neo4j 5 / Aura Free (Cypher 5).
// A fictional online shop: 15 components, 21 relationships.
// Convention: (a)-[:DEPENDS_ON]->(b) means "a needs b".
// Run each block separately in Neo4j Browser or the Aura query tool.
// =====================================================================

// ---------------------------------------------------------------------
// 0. Reset (only on a scratch database!)
// ---------------------------------------------------------------------
MATCH (n) DETACH DELETE n;

CREATE CONSTRAINT component_name IF NOT EXISTS
FOR (c:Component) REQUIRE c.name IS UNIQUE;

// ---------------------------------------------------------------------
// 1. Components
//    type: svc | data | queue | server | net
// ---------------------------------------------------------------------
UNWIND [
  {name:'web-frontend',   type:'svc',    team:'web'},
  {name:'mobile-bff',     type:'svc',    team:'mobile'},
  {name:'checkout',       type:'svc',    team:'commerce'},
  {name:'auth',           type:'svc',    team:'identity'},
  {name:'catalog',        type:'svc',    team:'catalog'},
  {name:'recs',           type:'svc',    team:'catalog'},
  {name:'payments',       type:'svc',    team:'commerce'},
  {name:'orders-db',      type:'data',   team:'platform'},
  {name:'session-cache',  type:'data',   team:'platform'},
  {name:'catalog-db',     type:'data',   team:'platform'},
  {name:'event-bus',      type:'queue',  team:'platform'},
  {name:'node-a',         type:'server', team:'infra'},
  {name:'node-b',         type:'server', team:'infra'},
  {name:'edge-router-04', type:'net',    team:'network', pool:'edge'},
  {name:'dns-internal',   type:'net',    team:'network'}
] AS row
MERGE (c:Component {name: row.name})
SET c.type = row.type, c.team = row.team, c.pool = row.pool;

// ---------------------------------------------------------------------
// 2. Relationships: DEPENDS_ON (needs) and HOSTED_ON (runs on)
// ---------------------------------------------------------------------
UNWIND [
  ['web-frontend','checkout'], ['web-frontend','auth'], ['web-frontend','catalog'],
  ['mobile-bff','auth'],       ['mobile-bff','recs'],
  ['checkout','auth'],         ['checkout','payments'], ['checkout','orders-db'],
  ['catalog','auth'],          ['catalog','catalog-db'],
  ['recs','catalog'],          ['auth','session-cache'], ['payments','event-bus'],
  ['node-a','edge-router-04'], ['node-a','dns-internal'],
  ['node-b','edge-router-04'], ['node-b','dns-internal']
] AS e
MATCH (a:Component {name: e[0]}), (b:Component {name: e[1]})
MERGE (a)-[:DEPENDS_ON]->(b);

UNWIND [
  ['orders-db','node-a'], ['session-cache','node-b'],
  ['catalog-db','node-b'], ['event-bus','node-a']
] AS e
MATCH (a:Component {name: e[0]}), (b:Component {name: e[1]})
MERGE (a)-[:HOSTED_ON]->(b);

// Check: expect 17 DEPENDS_ON + 4 HOSTED_ON = 21
MATCH ()-[r]->() RETURN type(r) AS type, count(*) AS n;

// Look at it:
MATCH p = ()-[:DEPENDS_ON|HOSTED_ON]->() RETURN p;

// ---------------------------------------------------------------------
// 3. BLAST RADIUS: walk AGAINST the arrows from the failed component
//    Expect 13 rows: node-a, node-b at 1 hop ... web-frontend at 4.
// ---------------------------------------------------------------------
MATCH (down:Component {name:'edge-router-04'})
MATCH p = (hit:Component)-[:DEPENDS_ON|HOSTED_ON]->{1,8}(down)
RETURN hit.name AS affected, hit.team AS team, min(length(p)) AS hops
ORDER BY hops, affected;

// ---------------------------------------------------------------------
// 4. ROOT CAUSE: walk WITH the arrows from the symptom
//    Expect 9 suspects: auth, orders-db, payments at 1 hop ...
// ---------------------------------------------------------------------
MATCH p = (:Component {name:'checkout'})-[:DEPENDS_ON|HOSTED_ON]->{1,8}(s:Component)
RETURN s.name AS suspect, s.type AS type, min(length(p)) AS hops
ORDER BY hops, suspect;

// ---------------------------------------------------------------------
// 5. CRITICALITY AUDIT: direct vs transitive dependents
//    Expect auth: 4 direct / 5 transitive;
//           edge-router-04 and dns-internal: 2 direct / 13 transitive.
// ---------------------------------------------------------------------
MATCH (c:Component)
RETURN c.name AS component,
       COUNT { (c)<-[:DEPENDS_ON|HOSTED_ON]-() } AS direct,
       size(COLLECT {
         MATCH (x)-[:DEPENDS_ON|HOSTED_ON]->{1,8}(c) RETURN DISTINCT x
       }) AS transitive
ORDER BY transitive DESC, direct DESC;

// ---------------------------------------------------------------------
// 6. STARTUP / DEPLOY WAVES (pure Cypher, no GDS needed)
//    Wave = 1 + longest path to something with no dependencies.
//    Expect 7 waves; mobile-bff starts last.
// ---------------------------------------------------------------------
MATCH (c:Component)
OPTIONAL MATCH p = (c)-[:DEPENDS_ON|HOSTED_ON]->+(leaf)
WHERE NOT (leaf)-[:DEPENDS_ON|HOSTED_ON]->()
WITH c, coalesce(max(length(p)), 0) + 1 AS wave
RETURN wave, collect(c.name) AS start_together
ORDER BY wave;

// ---------------------------------------------------------------------
// 7. ADD REDUNDANCY: a second router in the same pool
// ---------------------------------------------------------------------
MERGE (r:Component {name:'edge-router-05'})
SET r.type = 'net', r.team = 'network', r.pool = 'edge'
WITH r
MATCH (s:Component {type:'server'})
MERGE (s)-[:DEPENDS_ON]->(r);

// The plain walk in step 3 still says 13 affected, because it treats
// every dependency as "needs all of these". Ask the better question:
// which servers lose EVERY member of a pool when router-04 fails?
MATCH (s:Component {type:'server'})-[:DEPENDS_ON]->(r:Component)
WHERE r.pool IS NOT NULL
WITH s, r.pool AS pool, collect(r.name) AS members
WHERE all(m IN members WHERE m = 'edge-router-04')
RETURN s.name AS server_without_backup, pool;
// Expect: no rows. Delete router-05 again to see node-a and node-b return:
// MATCH (r:Component {name:'edge-router-05'}) DETACH DELETE r;

// ---------------------------------------------------------------------
// 8. BREAK IT: add a synchronous callback (creates a cycle)
// ---------------------------------------------------------------------
MATCH (p:Component {name:'payments'}), (c:Component {name:'checkout'})
MERGE (p)-[:DEPENDS_ON {mode:'sync-callback'}]->(c);

// Cycle check: run this in CI on every change to the dependency graph.
MATCH p = (a:Component)-[:DEPENDS_ON|HOSTED_ON]->{1,8}(a)
RETURN [n IN nodes(p) | n.name] AS cycle
LIMIT 5;
// Expect: checkout -> payments -> checkout (and the rotation starting at payments).

// Who can never start while the cycle exists? Anything on a cycle,
// plus anything that (transitively) needs something on a cycle.
MATCH (a:Component) WHERE (a)-[:DEPENDS_ON|HOSTED_ON]->{1,8}(a)
WITH collect(a) AS onCycle
MATCH (x:Component)
WHERE x IN onCycle
   OR any(c IN onCycle WHERE (x)-[:DEPENDS_ON|HOSTED_ON]->{1,8}(c))
RETURN collect(x.name) AS stuck;
// Expect 3: web-frontend, checkout, payments.

// ---------------------------------------------------------------------
// 9. FIX IT: replace the callback with an async event
//    (payments already publishes to event-bus; checkout subscribes)
// ---------------------------------------------------------------------
MATCH (:Component {name:'payments'})-[d:DEPENDS_ON {mode:'sync-callback'}]->(:Component {name:'checkout'})
DELETE d;
MATCH (c:Component {name:'checkout'}), (b:Component {name:'event-bus'})
MERGE (c)-[:SUBSCRIBES_TO {topic:'payment.completed'}]->(b);
// SUBSCRIBES_TO is deliberately a different type: it is a soft dependency
// and is not included in the blast-radius and wave queries above.
// Re-run the cycle check: no rows.

// ---------------------------------------------------------------------
// 10. OPTIONAL: Graph Data Science (AuraDS / self-managed with GDS)
//     GDS is not included in Aura Free.
// ---------------------------------------------------------------------
// Project with the arrows REVERSED, so "b before a" when a needs b:
MATCH (a:Component)-[:DEPENDS_ON|HOSTED_ON]->(b:Component)
WITH gds.graph.project('startOrder', b, a) AS g
RETURN g.graphName, g.nodeCount, g.relationshipCount;

CALL gds.dag.topologicalSort.stream('startOrder', {computeMaxDistanceFromSource: true})
YIELD nodeId, maxDistanceFromSource
RETURN gds.util.asNode(nodeId).name AS component,
       toInteger(maxDistanceFromSource) + 1 AS wave
ORDER BY wave, component;

// Articulation points and bridges (undirected view of the graph).
// In this platform both return nothing: every server has two network paths.
MATCH (a:Component)-[:DEPENDS_ON|HOSTED_ON]->(b:Component)
WITH gds.graph.project('undirected', a, b, {}, {undirectedRelationshipTypes:['*']}) AS g
RETURN g.graphName;
CALL gds.articulationPoints.stream('undirected') YIELD nodeId
RETURN gds.util.asNode(nodeId).name AS articulation_point;
CALL gds.bridges.stream('undirected') YIELD from, to
RETURN gds.util.asNode(from).name, gds.util.asNode(to).name;

CALL gds.graph.drop('startOrder', false);
CALL gds.graph.drop('undirected', false);

// ---------------------------------------------------------------------
// 11. PORTFOLIO STARTER: declared vs observed dependencies
//     Load declared edges from manifests as :DECLARES and observed edges
//     from traces as :CALLS, then diff them.
// ---------------------------------------------------------------------
// Undeclared calls (seen in traces, nobody wrote them down):
// MATCH (a:Component)-[:CALLS]->(b:Component)
// WHERE NOT (a)-[:DEPENDS_ON]->(b)
// RETURN a.name, b.name;
// Declared but never observed (stale config, or a path you never test):
// MATCH (a:Component)-[:DEPENDS_ON]->(b:Component)
// WHERE NOT (a)-[:CALLS]->(b)
// RETURN a.name, b.name;