Install
$ agentstack add skill-neo4j-contrib-neo4j-skills-neo4j-spring-data-skill ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
Neo4j Spring Data Skill
When to Use
- Configuring Spring Boot with Neo4j (
spring-boot-starter-data-neo4j) - Writing
@Nodeentity classes and@Relationship/@RelationshipPropertiesmappings - Defining
Neo4jRepositoryorReactiveNeo4jRepositoryinterfaces - Writing
@Queryannotations with Cypher on repository methods - Using Spring projections (interface-based, DTO, dynamic) with Neo4j
- Configuring
application.ymlfor Neo4j connection - Custom queries via
Neo4jClientorNeo4jTemplate - Spring AI
Neo4jVectorStorefor vector search in Spring apps - Transaction management, auditing, optimistic locking
When NOT to Use
- Raw Java driver without Spring →
neo4j-driver-java-skill - Cypher query authoring →
neo4j-cypher-skill - Driver version upgrades →
neo4j-migration-skill - GDS algorithms →
neo4j-gds-skill
Version Matrix
| SDN | Spring Boot | Spring Framework | Java | Neo4j | |--------|-------------|-----------------|------|-------| | 8.0.x | 3.3.x / 3.4.x | 6.2.x | 17+ | 5.15+ | | 8.1.x | 3.4.x+ | 7.0.x | 17+ | 5.15+ | | 7.5.x | 3.2.x | 6.1.x | 17+ | 4.4+ |
Use spring-boot-starter-data-neo4j — it pulls SDN + driver. No explicit SDN version needed when using Spring Boot BOM.
Setup
Maven
org.springframework.boot
spring-boot-starter-data-neo4j
Gradle
implementation 'org.springframework.boot:spring-boot-starter-data-neo4j'
Reactive stack (add alongside above)
org.springframework.boot
spring-boot-starter-webflux
Configuration
application.yml — imperative (standard)
spring:
neo4j:
uri: ${NEO4J_URI:bolt://localhost:7687}
authentication:
username: ${NEO4J_USERNAME:neo4j}
password: ${NEO4J_PASSWORD}
data:
neo4j:
database: ${NEO4J_DATABASE:neo4j}
application.yml — Aura (TLS required)
spring:
neo4j:
uri: ${NEO4J_URI} # neo4j+s://xxxx.databases.neo4j.io
authentication:
username: ${NEO4J_USERNAME:neo4j}
password: ${NEO4J_PASSWORD}
data:
neo4j:
database: ${NEO4J_DATABASE:neo4j}
Credentials: store in .env; never hardcode. Verify .env is in .gitignore.
Entity Mapping
import org.springframework.data.neo4j.core.schema.*;
// Internal generated ID (default for most cases)
@Node("Person")
public class PersonEntity {
@Id @GeneratedValue private Long id; // element ID (Long)
private String name;
@Property("birth_year") private Integer birthYear; // custom property name
@Relationship(type = "KNOWS", direction = Relationship.Direction.OUTGOING)
private List friends = new ArrayList<>();
}
// UUID business key
@Node("Product")
public class ProductEntity {
@Id @GeneratedValue(generatorClass = GeneratedValue.UUIDStringGenerator.class)
private String id;
@Version private Long version; // optimistic locking; required with business key
}
// User-assigned key (caller sets value; no @GeneratedValue)
@Node("Country")
public class CountryEntity {
@Id private String isoCode;
private String name;
}
// Multiple static labels
@Node(primaryLabel = "Vehicle", labels = {"Car", "Auditable"})
public class CarEntity { ... }
// Runtime labels
@Node("Content")
public class ContentEntity {
@Id @GeneratedValue private Long id;
@DynamicLabels private Set tags = new HashSet<>(); // labels added at runtime
}
Relationship Properties
Use @RelationshipProperties when the relationship itself carries data.
@RelationshipProperties
public class RolesRelationship {
@RelationshipId // internal relationship ID; required
private Long id;
private List roles;
@TargetNode // marks the other end of the relationship
private PersonEntity person;
}
@Node("Movie")
public class MovieEntity {
@Id @GeneratedValue
private Long id;
private String title;
@Relationship(type = "ACTED_IN", direction = Relationship.Direction.INCOMING)
private List actorsAndRoles = new ArrayList<>();
}
Repository Interfaces
Basic CRUD
import org.springframework.data.neo4j.repository.Neo4jRepository;
public interface PersonRepository extends Neo4jRepository {
Optional findByName(String name);
List findByBirthYearBetween(int from, int to);
List findByNameContainingIgnoreCase(String fragment);
long countByBirthYearGreaterThan(int year);
void deleteByName(String name);
}
@Query — custom Cypher
// CORRECT: $param bound parameter
@Query("MATCH (p:Person {name: $name})-[:KNOWS]->(f:Person) RETURN f")
List findFriendsOf(String name);
// With pagination
@Query(value = "MATCH (p:Person) RETURN p ORDER BY p.name",
countQuery = "MATCH (p:Person) RETURN count(p)")
Page findAllPaged(Pageable pageable);
// Return relationship-rich entity; map target via @Node return
@Query("MATCH (m:Movie) findMoviesActedInBy(String name);
Security rule: NEVER string-concatenate user input into Cypher. Always use $paramName.
Pagination and sorting
Page findByBirthYearGreaterThan(int year, Pageable pageable);
List findTop10ByOrderByNameAsc();
List findByName(String name, Sort sort);
Usage:
Pageable page = PageRequest.of(0, 20, Sort.by("name").ascending());
Page result = repo.findByBirthYearGreaterThan(1980, page);
Projections
Interface projection (closed — query-optimizable)
public interface PersonSummary {
String getName();
Integer getBirthYear();
}
List findByBirthYearLessThan(int year);
DTO projection (record — preferred in Java 17+)
public record PersonDto(String name, Integer birthYear) {}
List findByName(String name);
Dynamic projection
List findByName(String name, Class type);
// Usage
repo.findByName("Alice", PersonSummary.class);
repo.findByName("Alice", PersonEntity.class);
Open projection — SpEL (disables query optimization)
public interface FullName {
@Value("#{target.name + ' (' + target.birthYear + ')'}") String getDisplayName();
}
Reactive Repository
import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public interface ReactivePersonRepository extends ReactiveNeo4jRepository {
Mono findByName(String name);
@Query("MATCH (p:Person {name: $name})-[:KNOWS]->(f) RETURN f")
Flux findFriendsOf(String name);
}
Do NOT mix imperative and reactive database access in the same application context.
Custom Repository Implementation
Fragment pattern — use when @Query is not enough.
// 1. Fragment interface
public interface PersonRepositoryCustom {
List findByComplexCriteria(String criteria);
}
// 2. Impl — must end with "Impl"
public class PersonRepositoryCustomImpl implements PersonRepositoryCustom {
private final Neo4jClient neo4jClient;
PersonRepositoryCustomImpl(Neo4jClient c) { this.neo4jClient = c; }
@Override
public List findByComplexCriteria(String c) {
return new ArrayList<>(neo4jClient
.query("MATCH (p:Person) WHERE p.name CONTAINS $c RETURN p").bind(c).to("c")
.fetchAs(PersonEntity.class)
.mappedBy((t, r) -> { var e = new PersonEntity(); e.setName(r.get("p").asNode().get("name").asString()); return e; })
.all());
}
}
// 3. Compose
public interface PersonRepository extends Neo4jRepository, PersonRepositoryCustom {}
Neo4jClient — Low-Level Queries
Use when @Query is insufficient or you need full control over Cypher execution.
// Bind params + fetch single scalar
neo4jClient.query("MATCH (p:Person {name: $name}) RETURN count(*) AS cnt")
.bind("Alice").to("name")
.fetchAs(Long.class)
.mappedBy((t, r) -> r.get("cnt").asLong())
.one();
// Bind + run write (no result)
neo4jClient.query("MERGE (p:Person {name: $name})")
.bind(personName).to("name")
.run();
// Custom object mapping
neo4jClient.query("MATCH (p:Person)-[:DIRECTED]->(m:Movie) WHERE p.name=$n RETURN p, collect(m) AS movies")
.bind("Lilly Wachowski").to("n")
.fetchAs(Director.class)
.mappedBy((typeSystem, record) -> new Director(
record.get("p").asNode().get("name").asString(),
record.get("movies").asList(v -> new Movie(v.get("title").asString()))
)).one();
Full API: [references/neo4j-client.md](references/neo4j-client.md)
Transaction Management
@Service
@Transactional // class-level: all methods transactional
public class PersonService {
@Transactional(readOnly = true) // read-only hint
public Optional findByName(String name) { ... }
@Transactional // explicit write
public PersonEntity save(PersonEntity p) { return repository.save(p); }
}
Neo4jTransactionManager auto-configured. Do NOT mix with JPA PlatformTransactionManager without explicit qualifier. Use @Transactional on concrete class, not interface.
Spring AI — Neo4jVectorStore
Dependency
org.springframework.ai
spring-ai-starter-vector-store-neo4j
application.yml
spring:
ai:
vectorstore:
neo4j:
initialize-schema: true # creates vector index on first run
index-name: my-index
embedding-dimension: 1536 # must match your embedding model
distance-type: cosine # cosine (default) or euclidean
label: Document # node label for stored chunks
embedding-property: embedding # property for the vector
Requires Neo4j 5.15+. Reuses spring.neo4j.* connection config.
Usage
@Autowired VectorStore vectorStore;
// Store
vectorStore.add(List.of(new Document("text", Map.of("author", "alice"))));
// Similarity search
List results = vectorStore.similaritySearch(
SearchRequest.builder().query("spring neo4j").topK(5).similarityThreshold(0.75).build()
);
// With metadata filter
vectorStore.similaritySearch(
SearchRequest.builder().query("spring neo4j").topK(5)
.filterExpression("author == 'alice'").build()
);
Common Errors
| Error | Cause | Fix | |---|---|---| | MappingException: Could not find entity | Entity not scanned | Check @EnableNeo4jRepositories base package | | Relationships null after load | Default depth may skip deep rels | Use @Query with RETURN m, collect(r), collect(p) | | N+1 queries | Per-entity relationship fetch | Rewrite with single @Query; use projections | | OptimisticLockingFailureException | Stale @Version on concurrent write | Retry in service layer | | IllegalStateException: Cannot mix reactive/imperative | Both repo types in same context | Pick one stack | | Projection null fields | Getter name mismatch | Match getter to property name; check @Property alias | | @Query empty with rels | Missing collect(r), collect(p) | Return root node + rels + related nodes together | | Cannot delete node, node has relationships | deleteById without detach | Use @Query with DETACH DELETE | | Transaction not rolling back | @Transactional on interface | Apply on concrete service class |
Relationship Loading — Key Rule
SDN loads related entities eagerly up to a configured depth (default: 1 hop). For deeper graphs:
// Explicit @Query to control what gets loaded
@Query("""
MATCH (m:Movie) findByTitleWithCast(String title);
collect(r), collect(p) in RETURN is required for SDN to map @RelationshipProperties correctly.
References
- Spring Data Neo4j Reference (8.x)
- Spring AI Neo4jVectorStore
- GraphAcademy: Building Neo4j Apps with Spring Data
- Neo4j Getting Started — SDN
- SDN Advanced projections
- SDN Auditing
- [Modeling pitfalls, projection guide, type mapping](references/modeling-pitfalls.md)
Checklist
- [ ]
@Nodeuses explicit label string, not default class name - [ ]
@Id @GeneratedValue(or@Id+@Versionfor business key with optimistic lock) - [ ]
@RelationshipPropertiesclass has@RelationshipIdand@TargetNode - [ ]
@Relationshipdirection is explicit (OUTGOING / INCOMING) - [ ]
@QueryCypher uses$paramName— no string concatenation - [ ] Relationship-rich
@Queryreturnscollect(r), collect(p)alongside root node - [ ] Database name set in
application.yml(avoids default DB ambiguity) - [ ] Unique constraint exists in DB for any business key used in repository lookups
- [ ]
@Transactionalon concrete service class (not interface) - [ ] No imperative + reactive mix in same application context
- [ ] Credentials in env vars;
.envin.gitignore - [ ]
spring.ai.vectorstore.neo4j.initialize-schema: truefor first run (Spring AI)
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: neo4j-contrib
- Source: neo4j-contrib/neo4j-skills
- License: MIT
- Homepage: https://neo4j.com/llms.txt
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.