Spring Data Neo4j - Quick Reference
Deep Knowledge: Use mcp__documentation__fetch_docs with technology: spring-data-neo4j for comprehensive documentation.
Dependencies
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-neo4j</artifactId> </dependency>
Configuration
application.yml
spring: neo4j: uri: bolt://localhost:7687 authentication: username: neo4j password: ${NEO4J_PASSWORD}
data: neo4j: database: mydb # Neo4j 4.0+
Graph Concepts
┌─────────────────────────────────────────────────────────────┐ │ Graph Model │ │ │ │ ┌─────────┐ ┌─────────┐ │ │ │ Person │─────FOLLOWS────────▶│ Person │ │ │ │ (John) │ │ (Jane) │ │ │ └────┬────┘ └────┬────┘ │ │ │ │ │ │ WORKS_AT WORKS_AT │ │ │ │ │ │ ▼ ▼ │ │ ┌─────────┐ ┌─────────┐ │ │ │ Company │◀─────KNOWS───────────│ Person │ │ │ │ (Acme) │ │ (Bob) │ │ │ └─────────┘ └─────────┘ │ │ │ │ Nodes: Person, Company │ │ Relationships: FOLLOWS, WORKS_AT, KNOWS │ └─────────────────────────────────────────────────────────────┘
Node Entities
@Node("Person") public class Person {
@Id
@GeneratedValue
private Long id;
private String name;
private String email;
private LocalDate birthDate;
// Outgoing relationship
@Relationship(type = "FOLLOWS", direction = Direction.OUTGOING)
private Set<Person> following = new HashSet<>();
// Incoming relationship
@Relationship(type = "FOLLOWS", direction = Direction.INCOMING)
private Set<Person> followers = new HashSet<>();
// Relationship with properties
@Relationship(type = "WORKS_AT")
private WorksAt employment;
// Multiple relationships of same type
@Relationship(type = "KNOWS")
private List<Knows> connections = new ArrayList<>();
}
@Node("Company") public class Company {
@Id
@GeneratedValue
private Long id;
private String name;
private String industry;
@Relationship(type = "WORKS_AT", direction = Direction.INCOMING)
private Set<Person> employees = new HashSet<>();
}
Relationship Entities
@RelationshipProperties public class WorksAt {
@Id
@GeneratedValue
private Long id;
@TargetNode
private Company company;
private String position;
private LocalDate startDate;
private LocalDate endDate;
private BigDecimal salary;
}
@RelationshipProperties public class Knows {
@Id
@GeneratedValue
private Long id;
@TargetNode
private Person person;
private String context; // "work", "school", "family"
private LocalDate since;
private Integer trustLevel;
}
Repository Pattern
public interface PersonRepository extends Neo4jRepository<Person, Long> {
// Derived queries
Optional<Person> findByEmail(String email);
List<Person> findByNameContaining(String name);
// Custom Cypher queries
@Query("MATCH (p:Person)-[:FOLLOWS]->(f:Person) WHERE p.id = $personId RETURN f")
List<Person> findFollowing(Long personId);
@Query("MATCH (p:Person)<-[:FOLLOWS]-(f:Person) WHERE p.id = $personId RETURN f")
List<Person> findFollowers(Long personId);
@Query("""
MATCH (p:Person {id: $personId})-[:FOLLOWS*2..3]->(fof:Person)
WHERE NOT (p)-[:FOLLOWS]->(fof) AND p <> fof
RETURN DISTINCT fof
LIMIT $limit
""")
List<Person> findFriendsOfFriends(Long personId, int limit);
@Query("""
MATCH (p1:Person {id: $person1Id}), (p2:Person {id: $person2Id}),
path = shortestPath((p1)-[:KNOWS*]-(p2))
RETURN path
""")
List<Person> findShortestPath(Long person1Id, Long person2Id);
// Aggregations
@Query("""
MATCH (p:Person)-[:WORKS_AT]->(c:Company)
RETURN c.name as company, count(p) as employeeCount
ORDER BY employeeCount DESC
""")
List<CompanyStats> getCompanyStats();
// With relationship properties
@Query("""
MATCH (p:Person)-[w:WORKS_AT]->(c:Company)
WHERE p.id = $personId
RETURN p, w, c
""")
Person findWithEmployment(Long personId);
}
public interface CompanyRepository extends Neo4jRepository<Company, Long> {
@Query("""
MATCH (c:Company)<-[:WORKS_AT]-(p:Person)
WHERE c.id = $companyId
RETURN p
""")
List<Person> findEmployees(Long companyId);
}
Neo4jTemplate Operations
@Service @RequiredArgsConstructor public class GraphService {
private final Neo4jTemplate neo4jTemplate;
private final Neo4jClient neo4jClient;
// Save operations
public Person savePerson(Person person) {
return neo4jTemplate.save(person);
}
// Find by ID
public Optional<Person> findById(Long id) {
return neo4jTemplate.findById(id, Person.class);
}
// Custom queries with Neo4jClient
public List<Map<String, Object>> findMutualConnections(Long person1Id, Long person2Id) {
return neo4jClient.query("""
MATCH (p1:Person {id: $person1Id})-[:KNOWS]-(mutual:Person)-[:KNOWS]-(p2:Person {id: $person2Id})
RETURN mutual.name as name, mutual.email as email
""")
.bind(person1Id).to("person1Id")
.bind(person2Id).to("person2Id")
.fetch()
.all()
.stream()
.toList();
}
// Create relationship
public void createFollowRelationship(Long followerId, Long followeeId) {
neo4jClient.query("""
MATCH (a:Person {id: $followerId}), (b:Person {id: $followeeId})
MERGE (a)-[:FOLLOWS]->(b)
""")
.bind(followerId).to("followerId")
.bind(followeeId).to("followeeId")
.run();
}
// Delete relationship
public void removeFollowRelationship(Long followerId, Long followeeId) {
neo4jClient.query("""
MATCH (a:Person {id: $followerId})-[r:FOLLOWS]->(b:Person {id: $followeeId})
DELETE r
""")
.bind(followerId).to("followerId")
.bind(followeeId).to("followeeId")
.run();
}
// Complex graph traversal
public List<Person> findInfluencers(int minFollowers) {
return neo4jClient.query("""
MATCH (p:Person)<-[:FOLLOWS]-(follower:Person)
WITH p, count(follower) as followerCount
WHERE followerCount >= $minFollowers
RETURN p
ORDER BY followerCount DESC
""")
.bind(minFollowers).to("minFollowers")
.fetchAs(Person.class)
.mappedBy((typeSystem, record) -> {
// Custom mapping if needed
return neo4jTemplate.findById(
record.get("p").asNode().id(),
Person.class
).orElse(null);
})
.all()
.stream()
.filter(Objects::nonNull)
.toList();
}
}
Projections
// Interface projection public interface PersonSummary { String getName(); String getEmail(); int getFollowerCount(); }
// DTO projection public record PersonDto( Long id, String name, String email, List<String> followerNames ) {}
public interface PersonRepository extends Neo4jRepository<Person, Long> {
@Query("""
MATCH (p:Person)
WHERE p.id = $id
OPTIONAL MATCH (p)<-[:FOLLOWS]-(f:Person)
RETURN p.id as id, p.name as name, p.email as email,
collect(f.name) as followerNames
""")
Optional<PersonDto> findPersonDtoById(Long id);
}
Transactions
@Service @Transactional public class SocialNetworkService {
private final PersonRepository personRepository;
private final Neo4jClient neo4jClient;
@Transactional
public void transferFollowers(Long fromPersonId, Long toPersonId) {
// All operations in single transaction
neo4jClient.query("""
MATCH (from:Person {id: $fromId})<-[r:FOLLOWS]-(follower:Person)
MATCH (to:Person {id: $toId})
CREATE (follower)-[:FOLLOWS]->(to)
DELETE r
""")
.bind(fromPersonId).to("fromId")
.bind(toPersonId).to("toId")
.run();
}
@Transactional(readOnly = true)
public List<Person> findRecommendations(Long personId) {
// Read-only transaction
return personRepository.findFriendsOfFriends(personId, 10);
}
}
Reactive Support
public interface ReactivePersonRepository extends ReactiveNeo4jRepository<Person, Long> {
Flux<Person> findByNameContaining(String name);
@Query("MATCH (p:Person)-[:FOLLOWS]->(f:Person) WHERE p.id = $personId RETURN f")
Flux<Person> findFollowing(Long personId);
}
@Service public class ReactiveGraphService {
private final ReactiveNeo4jClient neo4jClient;
public Flux<Person> streamInfluencers() {
return neo4jClient.query("""
MATCH (p:Person)<-[:FOLLOWS]-(f:Person)
WITH p, count(f) as followers
WHERE followers > 100
RETURN p
ORDER BY followers DESC
""")
.fetchAs(Person.class)
.all();
}
}
Testing with Testcontainers
@SpringBootTest @Testcontainers class PersonRepositoryTest {
@Container
static Neo4jContainer<?> neo4j = new Neo4jContainer<>("neo4j:5")
.withAdminPassword("password");
@DynamicPropertySource
static void neo4jProperties(DynamicPropertyRegistry registry) {
registry.add("spring.neo4j.uri", neo4j::getBoltUrl);
registry.add("spring.neo4j.authentication.username", () -> "neo4j");
registry.add("spring.neo4j.authentication.password", neo4j::getAdminPassword);
}
@Autowired
private PersonRepository personRepository;
@Test
void shouldFindFollowers() {
Person john = personRepository.save(new Person("John"));
Person jane = personRepository.save(new Person("Jane"));
john.getFollowing().add(jane);
personRepository.save(john);
List<Person> followers = personRepository.findFollowers(jane.getId());
assertThat(followers).contains(john);
}
}
Best Practices
Do Don't
Model relationships explicitly Use arrays for connections
Use projections for partial data Fetch entire graph
Index frequently queried properties Query without indexes
Use MERGE for idempotent creates CREATE duplicates
Limit traversal depth Unbounded graph traversals
Production Checklist
-
Indexes on lookup properties
-
Constraints for uniqueness
-
Connection pooling configured
-
Transaction timeouts set
-
Query profiling enabled
-
Backup strategy defined
-
Cluster configuration (if HA)
-
Memory settings tuned
-
Monitoring enabled
-
Cypher query optimization
When NOT to Use This Skill
-
Raw Cypher queries - Consult Neo4j documentation directly
-
Relational data - Use spring-data-jpa for tabular data
-
Document storage - Use spring-data-mongodb
-
Simple key-value - Use spring-data-redis
Anti-Patterns
Anti-Pattern Problem Solution
Fetching entire graph Memory issues Use projections, limit depth
CREATE instead of MERGE Duplicate nodes Use MERGE for idempotent creates
Unbounded traversals Performance issues Add depth limits
Missing indexes Slow lookups Create indexes on lookup properties
Arrays for relationships Loses graph benefits Use proper @Relationship
Ignoring relationship direction Wrong query results Specify INCOMING/OUTGOING
Quick Troubleshooting
Problem Diagnostic Fix
Connection refused Check Neo4j running Start Neo4j, check bolt URI
Node not persisted Check @Node annotation Add annotation, verify ID
Relationship missing Check @Relationship Verify type and direction
Slow Cypher Use PROFILE/EXPLAIN Add indexes, optimize query
Circular reference Check entity graph Use @Relationship carefully
Reference Documentation
- Spring Data Neo4j Reference