SwadhyAI Notes

All System Design notes

System Design · Data Models

Graph Database Concepts

Fraud wants “accounts within three transfers of this device.” Recursive SQL joins explode into self-join soup. A with makes each hop a walk of stored neighbors. That is the real offer: cheap local hops, not “graphs are free at any depth.”

This note is the cost model: , , , partitions, and the split between OLTP graph queries and . Relational engines can store edges as tables; the question is which hop pattern you must make boring.

One multi-hop query

1

Index to a Person/Device, not the whole graph.

2

FOLLOWS, not every neighbor.

3

Degree × depth is the bill.

4

Remote hops turn into distributed BFS.

What actually happens

A stores nodes (records with a and properties) and edges (a , direction, and properties). With , the node record points at its incident edges. The first hop does not scan an edges table for from_id—it follows those pointers.

A query is a : start set → expand → filter → expand. Memory and CPU track the , not the number of pattern letters in Cypher. A on the path multiplies work. Sharding introduces : the pointer may be an RPC.

stores triples and answers SPARQL with covering triple indexes. runs global algorithms in supersteps. Mixing those with a latency-sensitive neighborhood API on one unplanned store is how graph projects stall.

Storage: adjacency versus edge tables

plaintext
Relational edge table hop:
  SELECT to_id FROM edges
   WHERE from_id = $id AND type = 'FOLLOWS'
  → B-tree lookup on (from_id, type)
  Fine for low degree; still an index probe per node in the frontier

Index-free adjacency hop:
  node_record.out_edges['FOLLOWS'] → linked records / packed neighbor ids
  → sequential walk of that node's typed list
  Constant factors win when you chain many hops on modest degrees

Both still pay O(degree). Neither makes hop-3 at degree 1e4 cheap.

Packed neighbor arrays (CSR-style, common in analytics graphs) are excellent for scans and terrible for high-churn OLTP edge inserts unless you leave slack or accept rewrites. Pointer-rich object graphs are the opposite: easy local mutation, worse cache behavior for bulk algorithms. Engines pick a side; you should know which you bought.

Properties on edges need a place to live. Fat edge payloads turn a neighborhood walk into a wide-row read. Keep traversal filters on cheap typed edges; store bulky payloads on nodes or in a side KV store keyed by edge id.

Traversal and frontier growth

Diagram text: the exact Mermaid definition below preserves the nodes, labels, and connections. Enable JavaScript to view the rendered diagram.
Read the diagram definition
flowchart TD
    S["Start: Person id=42"] --> E1["Expand FOLLOWS out"]
    E1 --> F1["Frontier hop-1"]
    F1 --> Filt["Filter: active = true"]
    Filt --> E2["Expand FOLLOWS out again"]
    E2 --> F2["Frontier hop-2"]
    F2 --> Dedup[Dedup + depth cap]
    Dedup --> Super{"Hit supernode?"}
    Super -->|yes| Budget["Apply degree budget / skip"]
    Super -->|no| Out[Return paths / nodes]
    Budget --> Out

Estimate before you promise an API. Average out-degree d, depth k, weak filters: frontier ≈ d^k before duplicate detection. Real social graphs have clustering, so the exponent is milder—until a sits in the middle.

plaintext
Friend-of-friend:
  start degree 300, hop-2 unique ≈ 300 * 300 * clustering
  clustering 0.1 → ~9,000 nodes, maybe tens of thousands of edge visits
  Easy on one machine if adjacency is local

Fraud 1..4 TRANSFERRED_TO:
  d=20, k=4 → 160,000 paths if tree-like
  one mule account with d=50,000 → that hop dominates
  Engine needs: maxDepth, maxNodes, relationship type, property filters
                applied during expansion, not after materializing all paths

Variable-length (1..*) without a cap:
  equivalent to asking the database to search until RAM dies

belong behind budgets. Shortest-path and bidirectional search help when both endpoints are known. “Find any interesting structure out there” is graph compute, not a web request.

Supernodes

A is a modeling problem first. If everyone connects to (:Country {name: US}), you built a star. Split edges by type and time: LIVES_IN_2026, or store the country as a property on the person instead of a hub node when you never traverse through it.

When the supernode is inherent (a celebrity, a popular listing), the engine must not expand all edges in one operator. Techniques: relationship grouping (edges sharded by type and by neighbor id range), approximate sampling for recommendations, precomputed neighborhoods for the hot vertex, and query planners that rewrite MATCH (c)-[]-(n) away from starting at c.

Detect

Track degree histograms. Alert when a vertex’s expansion dominates query CPU. Product graphs almost always have a power-law tail.

Contain

Type the edges, cap expansion, cache hot ego-graphs, and keep bulky metadata off the adjacency list so a degree walk stays a compact id list.

Partitioning and hop locality

Diagram text: the exact Mermaid definition below preserves the nodes, labels, and connections. Enable JavaScript to view the rendered diagram.
Read the diagram definition
flowchart LR
    subgraph M1["Machine A"]
        A[User 42]
        N1[Neighbors]
    end
    subgraph M2["Machine B"]
        X[Celebrity]
        N2[Millions of edges]
    end
    A -->|"local hop"| N1
    N1 -->|"remote hop"| X
    X -->|"explosion"| N2

A tries to keep edges inside a machine. Social graphs cluster; hash(userId) ignores that and maximizes the cut. Replicating a (vertex-cut) avoids shipping the hub but creates consistency work when its edges change.

Many production “graph” features stay single-shard: partition by tenant or by user and only traverse inside that subgraph. Global 3-hop on a billion-edge graph is a different product. If you need it, budget a distributed traversal layer (or precompute) instead of assuming the OLTP graph cluster will absorb it.

PlacementLocal hopsPain
Single machine / replica setAllCapacity ceiling; HA still needed
Hash vertices~1/N of neighborsDeep queries become scatter BFS
Community / tenant cutMost intra-tenantCross-tenant edges and imbalance
Vertex-cut supernodesHubs replicatedUpdate fan-out on the hub

OLTP graph versus graph compute

Online traversals have a start vertex, a small depth, and a latency SLO. has “all vertices,” iterative supersteps, and checkpointed shuffle. PageRank, community detection, and embedding pipelines belong on the compute side; they should write derived properties or tables that the OLTP graph or a KV cache can serve.

Pattern matching (Cypher, Gremlin) sits in between: a selective pattern can be OLTP; a poorly filtered pattern is an unbounded compute job accidentally issued by a product API. Treat query budgets like SQL timeouts, and log expansion counts the way you log rows scanned.

plaintext
OLTP:
  MATCH (p:Person {id:$id})-[:FOLLOWS]->(f)
        -[:FOLLOWS]->(fof)
  WHERE f.active AND fof.active
  RETURN fof.id LIMIT 50
  Budget: tens of ms, start indexed, types specified

Compute:
  foreach vertex: send rank/degree to neighbors
  iterate until delta < eps
  write pagerank property to serving store

Do not:
  MATCH (n)-[*]-(m) RETURN m
  on the production cluster at noon

When a graph engine is the right store

Strong fits: authorization graphs, fraud rings, network topology, knowledge graphs with bounded hops, recommendations that are actually path queries. Weak fits: tabular reporting, append-only telemetry, documents that are trees but are always fetched by primary key (a document store already nests trees).

You can implement a graph on relational tables with recursive CTEs or on a KV store with adjacency lists. That is valid when hop depth is 1–2 and degree is modest. Switch when the hop depth and mixed relationship types make adjacency a first-class layout, or when the query language’s pattern matcher is the product.

is a better fit when identity is a URI, predicates are shared vocabularies, and inference matters. Property graphs are a better fit when developers think in labeled nodes and typed edges with ad hoc properties. Translating between them is a project.

Worked micro-estimate

plaintext
Social OLTP cluster:
  200M people, avg 200 FOLLOWS edges
  edge record 24 B packed → 200M * 200 * 24 B ≈ 960 GB edges
  plus node properties 200M * 500 B ≈ 100 GB
  Replication 3× → ~3 TB disk, RAM for hot ego-graphs only

Query: 2-hop FOLLOWS from one user, degree 200, clustering 0.15
  visits ~ 200 + 200*200*0.15 ≈ 6,200 edges
  local pointer walk ~ few ms if cached
  hash-sharded 20 ways: expect many RPCs; p99 dominated by slow shard

Supernode (celebrity, 20M followers):
  expanding inbound FOLLOWS once = 20M edge visits
  not a web request; precompute or type-split (FOLLOWED_BY is not free either)

Budget RAM for:
  adjacency of recently active users
  not the whole 960 GB unless you paid for it

Trade-offs to weigh

Native graph engine versus edges in SQL/KV

Native adjacency and a pattern language win at mixed multi-hop OLTP. SQL plus a well-indexed edge table wins when hops are shallow, operators already live in the relational estate, and you need joins to non-graph facts in the same transaction. Do not migrate for the diagram.

Depth and expressiveness versus bounded latency

Variable-length paths are how you find rings. Unbounded paths are how you page the cluster. Put caps in the query, the driver, and the engine. Return partial results with a continuation if the product can stand it.

Hash partitions versus locality-aware cuts

Hashing scales writes and rebalance. Locality-aware placement scales traversals until communities shift. Many successful systems refuse global sharding and scale by tenant or by read replicas of a still-connected graph.

Fat properties on edges versus compact adjacency

Edge properties enable rich filters during a walk. Fat values destroy cache locality. Index properties you filter on; keep payloads elsewhere if the walk is the hot path.

OLTP serving versus graph compute

Serving hops and running PageRank fight for memory and locks. Snapshot the graph into a compute cluster, write derived scores back, and keep the online engine for targeted walks.

Choosing and explaining graph databases

A complete explanation connects the mechanism to a requirement. Describe what improves when you introduce it, which new failure cases become possible, and how you would know that the trade-off is worthwhile.

Explain the core idea

A property graph stores typed nodes and edges; index-free adjacency makes a local hop a neighbor-list walk instead of a global join.

Query cost is frontier size times per-edge work, made worse by supernodes and remote partitions.

Use a graph engine when multi-hop typed patterns are the product; otherwise an edge table or nested documents may be enough.

When it belongs

Fraud, authz, recommendations, or topology questions that are naturally paths.

Recursive SQL becomes the bottleneck and hop depth is greater than one or two with mixed types.

You must explain why “just 3 hops” is not O(1).

How it fails

Supernode expansion blowing CPU and RAM.

Unbounded variable-length matches as user-facing queries.

Hash partitioning turning every deep query into a distributed BFS.

Running full-graph algorithms on the OLTP cluster.

Trade-offs to weigh

Native adjacency versus staying in SQL for operational simplicity.

Partition locality versus rebalance simplicity.

Rich edge properties versus compact walks.

Common misunderstandings

Claiming graph databases make any relationship query constant time.

Drawing a graph on the whiteboard and stopping at “we’ll use Neo4j” without a hop budget.

Confusing RDF triple stores with property-graph OLTP, or OLTP graphs with Pregel compute.

Modeling every attribute as a node and creating accidental supernodes.

Wrap-up

Graph stores earn their keep when the cheap question is “who is connected, through these edge types, within this budget.” Adjacency, typed relationships, and start indexes make hops local. Frontiers, supernodes, and cuts decide whether that stays true at product scale.

Key takeaway

Hops are pointer walks only while neighbors are local, typed, and bounded. Estimate the frontier, isolate supernodes, and keep global algorithms off the request path.