Back openDesk Edu for a sovereign, open-source education β every vote counts.
Vote nowTraditional fraud detection relies on rule-based systems and machine learning on tabular data β flagging transactions above a threshold, detecting unusual login locations, or scoring credit applications with logistic regression. These approaches work well for individual bad actors. They fail against organised fraud: collusion rings, synthetic identity networks, circular money laundering, and account takeover cascades.
Fraud rings are fundamentally graph structures. A shared phone number across fifty accounts isn't a column value β it is a connection. A chain of transactions that cycles back to the origin isn't a set of rows β it is a directed cycle. A fraudster controlling multiple synthetic identities isn't a pattern in any single table β it is a star-shaped subgraph of shared attributes.
Property graphs capture these relational patterns natively. Every transaction, device fingerprint, IP address, phone number, and account becomes a node. Every relationship β SENT, USES_DEVICE, SHARES_IP β becomes an edge. Once your data lives in a graph, the algorithms that detect fraud become graph algorithms. This article walks through a complete fraud detection pipeline using Neo4j and its Graph Data Science (GDS) library.
The foundation of any graph-based fraud system is the data model. Every entity that can be shared or connected across accounts becomes a node. Every connection becomes a relationship.
Core entities:
| Entity | Description | Example Properties |
|---|---|---|
Customer | Account holder or business | id, name, createdAt, riskScore |
Transaction | Financial transfer | amount, currency, timestamp, status |
Device | Browser or mobile fingerprint | fingerprint, deviceType, os |
IPAddress | Network origin | address, asn, country |
PhoneNumber | Contact number | number, carrier, isPrepaid |
Address | Physical location | street, city, postcode, country |
Relationships:
// Financial flows
(c1:Customer)-[:SENT {amount, timestamp, currency}]->(t:Transaction)
(t:Transaction)-[:RECEIVED_BY]->(c2:Customer)
// Shared attributes (fraud indicators)
(c:Customer)-[:USES_DEVICE {firstSeen, lastSeen}]->(d:Device)
(c:Customer)-[:USES_IP {firstSeen, lastSeen}]->(ip:IPAddress)
(c:Customer)-[:HAS_PHONE]->(p:PhoneNumber)
(c:Customer)-[:HAS_ADDRESS]->(a:Address)
A shared device between two accounts that otherwise have no connection is a strong fraud signal. In a relational database, detecting this requires joining through a device-account intersection table. In Cypher, it is a single traversal:
MATCH (fraud:Customer {id: $fraudId})-[:USES_DEVICE]->(d:Device)
<-[:USES_DEVICE]-(suspected:Customer)
WHERE suspected.id <> $fraudId
RETURN suspected.id AS accountId,
d.fingerprint AS deviceFingerprint,
d.firstSeen AS deviceFirstSeen
This query executes in milliseconds on graphs with billions of nodes when backed by an index on :Customer(id).
Money laundering frequently involves circular flows: A sends to B, B sends to C, C sends back to A. In a graph, this maps naturally onto a directed cycle of length three or more.
// Detect 3-step money laundering cycles
MATCH p = (a:Customer)-[:SENT]->(t1:Transaction)-[:RECEIVED_BY]->(b:Customer)
-[:SENT]->(t2:Transaction)-[:RECEIVED_BY]->(c:Customer)
-[:SENT]->(t3:Transaction)-[:RECEIVED_BY]->(a)
WHERE a.id < b.id AND b.id < c.id
AND t1.amount > 10000
AND t2.amount > 10000
AND t3.amount > 10000
AND t1.timestamp <= t2.timestamp
AND t2.timestamp <= t3.timestamp
RETURN a.id, b.id, c.id,
t1.amount + t2.amount + t3.amount AS totalCirculated,
duration.between(t1.timestamp, t3.timestamp).hours AS cycleDurationHours
ORDER BY totalCirculated DESC
LIMIT 50
The a.id < b.id AND b.id < c.id guard prevents duplicate permutations of the same cycle. This pattern extends naturally to four-step and five-step cycles by adding more segments.
Synthetic identity fraud creates accounts that share a small set of attributes β the same phone number, device, and address β while appearing unrelated in the financial flow graph. This forms a star topology.
// Find IP addresses shared by an unusually high number of accounts
MATCH (c:Customer)-[:USES_IP]->(ip:IPAddress)
WITH ip, count(c) AS accountCount
WHERE accountCount > 5
MATCH (c:Customer)-[:USES_IP]->(ip)
RETURN ip.address AS sharedIP,
accountCount,
collect(c.id)[..10] AS sampleAccounts,
collect(DISTINCT c.riskScore) AS riskScores
ORDER BY accountCount DESC
A single IP address shared by ten accounts, where those accounts have different addresses and phone numbers, is worth immediate investigation. When combined with a prepaid phone number and a newly created device fingerprint, it becomes a near-certain fraud indicator.
Cypher queries catch known patterns. Graph Data Science algorithms catch unknown patterns by scoring every node and community in the graph.
The typical GDS fraud pipeline has four stages:
CALL gds.graph.project(
'fraud-graph',
'Customer',
{
SENT: { orientation: 'UNDIRECTED' },
USES_IP: { orientation: 'UNDIRECTED' },
USES_DEVICE: { orientation: 'UNDIRECTED' }
}
)
Using UNDIRECTED orientation for shared-attribute edges makes the graph topology symmetric β important for community detection algorithms that do not consider edge direction.
Fraudsters who control many synthetic accounts will naturally become high-PageRank nodes, because their synthetic accounts all connect to shared infrastructure (devices, IPs, phones) that they also connect to.
CALL gds.pageRank.write('fraud-graph', {
maxIterations: 100,
dampingFactor: 0.85,
writeProperty: 'fraudPageRank'
})
Accounts with a fraudPageRank in the top 1% of the distribution, combined with a recent creation date, are candidates for manual review.
Legitimate customers form small, dense communities (family groups, colleagues, neighbours). Fraud rings also form dense communities β but with a distinctive signature: high internal connectivity combined with few external connections.
CALL gds.louvain.write('fraud-graph', {
maxLevels: 5,
writeProperty: 'communityId'
})
// Find suspiciously small, dense communities
MATCH (c:Customer)
WITH c.communityId AS communityId,
count(c) AS communitySize,
collect(c.fraudPageRank) AS pageRanks
WHERE communitySize >= 3 AND communitySize <= 20
RETURN communityId,
communitySize,
apoc.coll.percentiles(pageRanks, [50, 90]) AS pageRankPercentiles
ORDER BY communitySize ASC
LIMIT 20
A community of 5β20 accounts with a shared device, high internal PageRank, and no transaction history among members is a textbook synthetic identity ring.
| Dimension | Rule-Based / ML | Graph-Based |
|---|---|---|
| Data model | Flat tables, feature vectors | Property graph with typed edges |
| Collusion detection | Requires engineered pair features | Natural via shared-attribute traversal |
| Novel fraud patterns | Requires retraining | Zero-day detection via graph structure |
| Synthetic identity | Hard β identical features blend in | Easy β star patterns are directly visible |
| Circular laundering | Impossible without graph traversal | Trivial cycle detection in Cypher |
| Explainability | Black-box model scores | Traversable evidence paths |
| Real-time scoring | Yes, with feature store | Hybrid β rules for RT, GDS for batch |
GDS algorithms like PageRank and Louvain operate on static snapshots. In a fraud context where new transactions arrive every second, two complementary strategies exist:
Most production deployments use both: fast Cypher checks at request time, periodic GDS scoring for deep analysis.
Graph-derived scores β PageRank, community ID, shared-attribute count β become features in your broader ML pipeline. Export them to your feature store (Feast, Tecton) so that real-time inference models consume them alongside tabular features.
# Example: Export PageRank scores to a feature store
import neo4j
from feast import FeatureStore
driver = neo4j.GraphDatabase.driver("bolt://localhost:7687", auth=("neo4j", "password"))
records, _, _ = driver.execute_query(
"MATCH (c:Customer) RETURN c.id AS customer_id, c.fraudPageRank AS page_rank"
)
store = FeatureStore(repo_path="./feature_repo")
store.write_to_online_store(
feature_table="customer_graph_features",
df=pd.DataFrame.from_records(records)
)
A fraud graph for a mid-sized fintech can reach 100M+ nodes and 1B+ relationships. Mitigations include:
A shared IP address from 2018 is far less suspicious than one shared last week. Model temporal edges with weight properties that decay with age, or restrict projections to recent windows.
// Weight edges by recency β more recent = higher weight
MATCH (c:Customer)-[r:USES_IP]->(ip:IPAddress)
SET r.weight = exp(-duration.between(r.lastSeen, date()).days / 30.0)
This exponential decay halves the weight every 21 days, ensuring old shared attributes contribute little to algorithm scores.
Fraud is fundamentally relational. Organised fraud rings, synthetic identity networks, circular money laundering, and coordinated bot attacks all produce graph structures that are invisible in tabular data. By modelling your fraud domain as a property graph and applying graph algorithms β community detection, centrality scoring, and cycle detection β you can detect these patterns directly.
The combination of Neo4j's Cypher query language for pattern matching and the GDS library for scalable algorithm execution provides a complete toolkit for graph-powered fraud detection. When integrated with existing ML pipelines through feature stores, graph signals can lift fraud detection performance by 20β40% in production deployments β particularly for the collusion and synthetic identity cases that traditional models miss.
For teams starting out: model your data as a graph first, even if you only run Cypher queries. The act of connecting accounts through shared devices, IPs, and transaction flows will reveal fraud patterns that no feature engineering on flat tables can match. Add GDS algorithms once your graph reaches a scale where manual querying becomes impractical β by that point, you will already have the evidence that graph is the right model for the job.