AgentStack
SKILL verified MIT Self-run

Neo4j Spring Data Skill

skill-neo4j-contrib-neo4j-skills-neo4j-spring-data-skill · by neo4j-contrib

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

No reviews yet
0 installs
6 views
0.0% view→install

Install

$ agentstack add skill-neo4j-contrib-neo4j-skills-neo4j-spring-data-skill

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No 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.

Are you the author of Neo4j Spring Data Skill? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

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 Springneo4j-driver-java-skill
  • Cypher query authoringneo4j-cypher-skill
  • Driver version upgradesneo4j-migration-skill
  • GDS algorithmsneo4j-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


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.

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

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.