# Neo4j Spring Data Skill

> Use when building Spring Boot applications with Neo4j using Spring Data Neo4j (SDN 7.x/8.x):

- **Type:** Skill
- **Install:** `agentstack add skill-neo4j-contrib-neo4j-skills-neo4j-spring-data-skill`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [neo4j-contrib](https://agentstack.voostack.com/s/neo4j-contrib)
- **Installs:** 0
- **Category:** [Databases](https://agentstack.voostack.com/c/databases)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [neo4j-contrib](https://github.com/neo4j-contrib)
- **Source:** https://github.com/neo4j-contrib/neo4j-skills/tree/main/neo4j-spring-data-skill
- **Website:** https://neo4j.com/llms.txt

## Install

```sh
agentstack add skill-neo4j-contrib-neo4j-skills-neo4j-spring-data-skill
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# Neo4j Spring Data Skill

## When to Use

- Configuring Spring Boot with Neo4j (`spring-boot-starter-data-neo4j`)
- Writing `@Node` entity classes and `@Relationship`/`@RelationshipProperties` mappings
- Defining `Neo4jRepository` or `ReactiveNeo4jRepository` interfaces
- Writing `@Query` annotations with Cypher on repository methods
- Using Spring projections (interface-based, DTO, dynamic) with Neo4j
- Configuring `application.yml` for Neo4j connection
- Custom queries via `Neo4jClient` or `Neo4jTemplate`
- Spring AI `Neo4jVectorStore` for 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

```xml

    org.springframework.boot
    spring-boot-starter-data-neo4j

```

### Gradle

```gradle
implementation 'org.springframework.boot:spring-boot-starter-data-neo4j'
```

### Reactive stack (add alongside above)

```xml

    org.springframework.boot
    spring-boot-starter-webflux

```

---

## Configuration

### application.yml — imperative (standard)

```yaml
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)

```yaml
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

```java
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.

```java
@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;
}
```

```java
@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

```java
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

```java
// 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

```java
Page findByBirthYearGreaterThan(int year, Pageable pageable);

List findTop10ByOrderByNameAsc();

List findByName(String name, Sort sort);
```

Usage:

```java
Pageable page = PageRequest.of(0, 20, Sort.by("name").ascending());
Page result = repo.findByBirthYearGreaterThan(1980, page);
```

---

## Projections

### Interface projection (closed — query-optimizable)

```java
public interface PersonSummary {
    String getName();
    Integer getBirthYear();
}

List findByBirthYearLessThan(int year);
```

### DTO projection (record — preferred in Java 17+)

```java
public record PersonDto(String name, Integer birthYear) {}

List findByName(String name);
```

### Dynamic projection

```java
 List findByName(String name, Class type);

// Usage
repo.findByName("Alice", PersonSummary.class);
repo.findByName("Alice", PersonEntity.class);
```

### Open projection — SpEL (disables query optimization)

```java
public interface FullName {
    @Value("#{target.name + ' (' + target.birthYear + ')'}") String getDisplayName();
}
```

---

## Reactive Repository

```java
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.

```java
// 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.

```java
// 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

```java
@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

```xml

    org.springframework.ai
    spring-ai-starter-vector-store-neo4j

```

### application.yml

```yaml
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

```java
@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:

```java
// 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)](https://docs.spring.io/spring-data/neo4j/reference/)
- [Spring AI Neo4jVectorStore](https://docs.spring.io/spring-ai/reference/api/vectordbs/neo4j.html)
- [GraphAcademy: Building Neo4j Apps with Spring Data](https://graphacademy.neo4j.com/courses/app-spring-data/)
- [Neo4j Getting Started — SDN](https://neo4j.com/docs/getting-started/languages-guides/java/spring-data-neo4j/)
- [SDN Advanced projections](https://docs.spring.io/spring-data/neo4j/reference/repositories/projections.html)
- [SDN Auditing](https://docs.spring.io/spring-data/neo4j/reference/object-mapping/auditing.html)
- [Modeling pitfalls, projection guide, type mapping](references/modeling-pitfalls.md)

---

## Checklist

- [ ] `@Node` uses explicit label string, not default class name
- [ ] `@Id @GeneratedValue` (or `@Id` + `@Version` for business key with optimistic lock)
- [ ] `@RelationshipProperties` class has `@RelationshipId` and `@TargetNode`
- [ ] `@Relationship` direction is explicit (OUTGOING / INCOMING)
- [ ] `@Query` Cypher uses `$paramName` — no string concatenation
- [ ] Relationship-rich `@Query` returns `collect(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
- [ ] `@Transactional` on concrete service class (not interface)
- [ ] No imperative + reactive mix in same application context
- [ ] Credentials in env vars; `.env` in `.gitignore`
- [ ] `spring.ai.vectorstore.neo4j.initialize-schema: true` for 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](https://github.com/neo4j-contrib)
- **Source:** [neo4j-contrib/neo4j-skills](https://github.com/neo4j-contrib/neo4j-skills)
- **License:** MIT
- **Homepage:** https://neo4j.com/llms.txt

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** yes
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-neo4j-contrib-neo4j-skills-neo4j-spring-data-skill
- Seller: https://agentstack.voostack.com/s/neo4j-contrib
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
