Graph databases have rapidly gained popularity in modern software architecture, as systems increasingly rely on relationships, recommendations, and connected data. From social media platforms and fraud detection systems to recommendation engines and knowledge graphs, graph databases offer a powerful way to model and traverse complex relationships that are hard to express efficiently in relational databases.
This second part of the series narrows the focus to Neo4j, the market’s most prominent graph database engine. We’ll explore its architecture, query language (Cypher), and see how Java developers can leverage Eclipse JNoSQL 1.1.8 to integrate it seamlessly into Java applications.
Understanding Neo4j
Neo4j is a native graph database explicitly built to store and process graphs efficiently and effectively. It represents data as nodes (vertices) and relationships (edges), which can hold properties. Unlike relational databases, where relationships are inferred through foreign keys, Neo4j treats relationships as first-class citizens, resulting in faster and more expressive graph traversals.
Some of the key features that make Neo4j popular include:
- A powerful query language: Cypher
- ACID-compliant transactional model
- High-performance graph traversals
- Visual data browser and mature tooling
- Strong community and commercial support
Meet Cypher: The Graph Query Language
Cypher is Neo4j’s declarative query language designed to express graph patterns intuitively. Its syntax is familiar to SQL users but is intended to traverse nodes and relationships, not join tables.
Here’s a quick comparison:
Feature | SQL | Cypher |
---|---|---|
Entity Retrieval | SELECT * FROM Book | MATCH (b:Book) RETURN b |
Filtering | WHERE name=”Java” | WHERE b.name=”Java” |
Join/Relationship | JOIN Book_Category ON… | MATCH (b:Book)-[:is]->(c:Category) RETURN b |
Grouping & Count | GROUP BY category_id | WITH c, count(b) AS total |
Schema Flexibility | Fixed schema | Property graph, more dynamic |
Getting Started With Neo4j and Eclipse JNoSQL
Eclipse JNoSQL simplifies database integration by adhering to Jakarta EE specifications — specifically, Jakarta NoSQL and Jakarta Data. In this sample, we’ll use Java SE and showcase how to interact with Neo4j using a domain model of books and their categories.
First, ensure Neo4j is running. Use Docker to spin it up quickly:
docker run --publish=7474:7474 --publish=7687:7687 --env NEO4J_AUTH=neo4j/admin123 neo4j:5.26.3
Now, configure the connection using MicroProfile Config (which supports environment variable overrides):
jnosql.neo4j.uri=bolt://localhost:7687
jnosql.neo4j.username=neo4j
jnosql.neo4j.password=admin123
jnosql.graph.database=neo4j
You can overwrite any configuration thanks to the Twelve Application Factor. For example, you can update the production password without changing a single line of code. What you need to do is set the System environment:
export JNOSQL_NEO4J_PASSWORD=PRODUCTION_PASSWORD
Modeling Entities
With Neo4j configured and running, the next step is to define our domain model using Jakarta NoSQL annotations. In this example, we focus on two entities — Book
and Category
— which will form the core nodes in our graph. These classes will demonstrate how to insert data and define relationships using Neo4j in a clean, idiomatic way with Java.
@Entity
public class Book {
@Id
private String id;
@Column
private String name;
}
@Entity
public class Category {
@Id
private String id;
@Column
private String name;
}
Eclipse JNoSQL offers a Neo4JTemplate
, a specialization of Template
, for native Neo4j access. This API allows direct interactions with Neo4j using Cypher queries, edge creation, and entity persistence programmatically and expressively.
Here’s how you can encapsulate persistence logic in a basic service layer:
@ApplicationScoped
public class BookService {
private static final Logger LOGGER = Logger.getLogger(BookService.class.getName());
@Inject
private Neo4JTemplate template;
public Book save(Book book) {
Optional found = template.select(Book.class).where("name").eq(book.getName()).singleResult();
return found.orElseGet(() -> template.insert(book));
}
public Category save(Category category) {
Optional found = template.select(Category.class).where("name").eq(category.getName()).singleResult();
return found.orElseGet(() -> template.insert(category));
}
}
To demonstrate a full execution cycle, we use the BookApp
class, which initializes a CDI container, stores books and categories, creates edges between them, and runs Cypher queries:
public final class BookApp {
private BookApp() {}
public static void main(String[] args) {
try (SeContainer container = SeContainerInitializer.newInstance().initialize()) {
var template = container.select(Neo4JTemplate.class).get();
var service = container.select(BookService.class).get();
var software = service.save(Category.of("Software"));
var java = service.save(Category.of("Java"));
var architecture = service.save(Category.of("Architecture"));
var performance = service.save(Category.of("Performance"));
var effectiveJava = service.save(Book.of("Effective Java"));
var cleanArchitecture = service.save(Book.of("Clean Architecture"));
var systemDesign = service.save(Book.of("System Design Interview"));
var javaPerformance = service.save(Book.of("Java Performance"));
template.edge(Edge.source(effectiveJava).label("is").target(java).property("relevance", 10).build());
template.edge(Edge.source(effectiveJava).label("is").target(software).property("relevance", 9).build());
template.edge(Edge.source(cleanArchitecture).label("is").target(software).property("relevance", 8).build());
template.edge(Edge.source(cleanArchitecture).label("is").target(architecture).property("relevance", 10).build());
template.edge(Edge.source(systemDesign).label("is").target(architecture).property("relevance", 9).build());
template.edge(Edge.source(systemDesign).label("is").target(software).property("relevance", 7).build());
template.edge(Edge.source(javaPerformance).label("is").target(performance).property("relevance", 8).build());
template.edge(Edge.source(javaPerformance).label("is").target(java).property("relevance", 9).build());
System.out.println("Books in 'Architecture' category:");
var architectureBooks = template.cypher("MATCH (b:Book)-[:is]->(c:Category {name: 'Architecture'}) RETURN b AS book", Collections.emptyMap()).toList();
architectureBooks.forEach(doc -> System.out.println(" - " + doc));
System.out.println("Categories with more than one book:");
var commonCategories = template.cypher("MATCH (b:Book)-[:is]->(c:Category) WITH c, count(b) AS total WHERE total > 1 RETURN c", Collections.emptyMap()).toList();
commonCategories.forEach(doc -> System.out.println(" - " + doc));
var highRelevanceBooks = template.cypher("MATCH (b:Book)-[r:is]->(:Category) WHERE r.relevance >= 9 RETURN DISTINCT b", Collections.emptyMap()).toList();
System.out.println(" Books with high relevance:");
highRelevanceBooks.forEach(doc -> System.out.println(" - " + doc));
System.out.println(" Books with name: 'Effective Java':");
var effectiveJavaBooks = template.cypher("MATCH (b:Book {name: $name}) RETURN b", Collections.singletonMap("name", "Effective Java")).toList();
effectiveJavaBooks.forEach(doc -> System.out.println(" - " + doc));
}
}
}
You can also develop relationships and execute Cypher queries. The example below shows how to define an edge between two entities using the explicitly Edge
API provided by Eclipse JNoSQL. This edge represents a relationship with the label is
and includes a property, relevance
to express its importance.
Edge edge = Edge.source(book).label("is").target(category).property("relevance", 9).build();
template.edge(edge);
After creating edges, you can use Cypher to query the graph. For instance, the following query retrieves books that have a high relevance relationship (>= 9) to any category:
var books = template.cypher(
"MATCH (b:Book)-[r:is]->(:Category) WHERE r.relevance >= 9 RETURN DISTINCT b",
Collections.emptyMap()
).toList();
These examples demonstrate how Neo4JTemplate can persist and relate domain entities and navigate and analyze graph structures with Cypher.
Refer to BookApp
in the sample for data setup, insertion, relationship creation, and Cypher queries. After inserting that information into the database, you can check the Ne4J dashboard:
You can also interact with Neo4j using repository interfaces. Eclipse JNoSQL supports Neo4JRepository
a Jakarta Data extension with Cypher support:
@Repository
public interface BookRepository extends Neo4JRepository {
Optional findByName(String name);
@Cypher("MATCH (b:Book)-[:is]->(c:Category {name: 'Architecture'}) RETURN DISTINCT b")
List findArchitectureBooks();
@Cypher("MATCH (b:Book)-[r:is]->(:Category) WHERE r.relevance >= 9 RETURN DISTINCT b")
List highRelevanceBooks();
}
@Repository
public interface CategoryRepository extends Neo4JRepository {
Optional findByName(String name);
@Cypher("MATCH (b:Book)-[:is]->(c:Category) WITH c, count(b) AS total WHERE total > 1 RETURN c")
List commonCategories();
}
The BookApp2
class demonstrates how to use these repositories in practice by replacing the low-level Neo4JTemplate
usage with Jakarta Data’s repository abstraction. This approach simplifies the code significantly while allowing access to Cypher’s expressive power through annotations.
This example not only shows how to persist entities using standard repository methods like findByName
, but also how to perform complex graph queries through the @Cypher
annotation. Additionally, the edge creation is still handled via GraphTemplate
, keeping the relationship modeling fully explicit and under control.
This dual-model — repositories for domain access and templates for graph-specific relationships — offers a great balance between convenience and flexibility, making it ideal for complex domain models with rich relationships.
import jakarta.enterprise.inject.se.SeContainer;
import jakarta.enterprise.inject.se.SeContainerInitializer;
import org.eclipse.jnosql.mapping.graph.Edge;
import org.eclipse.jnosql.mapping.graph.GraphTemplate;
public final class BookApp2 {
private BookApp2() {
}
public static void main(String[] args) {
try (SeContainer container = SeContainerInitializer.newInstance().initialize()) {
var template = container.select(GraphTemplate.class).get();
var bookRepository = container.select(BookRepository.class).get();
var repository = container.select(CategoryRepository.class).get();
var software = repository.findByName("Software").orElseGet(() -> repository.save(Category.of("Software")));
var java = repository.findByName("Java").orElseGet(() -> repository.save(Category.of("Java")));
var architecture = repository.findByName("Architecture").orElseGet(() -> repository.save(Category.of("Architecture")));
var performance = repository.findByName("Performance").orElseGet(() -> repository.save(Category.of("Performance")));
var effectiveJava = bookRepository.findByName("Effective Java").orElseGet(() -> bookRepository.save(Book.of("Effective Java")));
var cleanArchitecture = bookRepository.findByName("Clean Architecture").orElseGet(() -> bookRepository.save(Book.of("Clean Architecture")));
var systemDesign = bookRepository.findByName("System Design Interview").orElseGet(() -> bookRepository.save(Book.of("System Design Interview")));
var javaPerformance = bookRepository.findByName("Java Performance").orElseGet(() -> bookRepository.save(Book.of("Java Performance")));
template.edge(Edge.source(effectiveJava).label("is").target(java).property("relevance", 10).build());
template.edge(Edge.source(effectiveJava).label("is").target(software).property("relevance", 9).build());
template.edge(Edge.source(cleanArchitecture).label("is").target(software).property("relevance", 8).build());
template.edge(Edge.source(cleanArchitecture).label("is").target(architecture).property("relevance", 10).build());
template.edge(Edge.source(systemDesign).label("is").target(architecture).property("relevance", 9).build());
template.edge(Edge.source(systemDesign).label("is").target(software).property("relevance", 7).build());
template.edge(Edge.source(javaPerformance).label("is").target(performance).property("relevance", 8).build());
template.edge(Edge.source(javaPerformance).label("is").target(java).property("relevance", 9).build());
System.out.println("Books in 'Architecture' category:");
var architectureBooks = bookRepository.findArchitectureBooks();
architectureBooks.forEach(doc -> System.out.println(" - " + doc));
System.out.println("Categories with more than one book:");
var commonCategories = repository.commonCategories();
commonCategories.forEach(doc -> System.out.println(" - " + doc));
var highRelevanceBooks = bookRepository.highRelevanceBooks();
System.out.println("Books with high relevance:");
highRelevanceBooks.forEach(doc -> System.out.println(" - " + doc));
var bookByName = bookRepository.queryByName("Effective Java");
System.out.println("Book by name: " + bookByName);
}
}
}
Conclusion
Neo4j offers powerful graph capabilities that Java developers can now access in a clean, standard way using Eclipse JNoSQL and Jakarta Data. Whether you choose to interact via Neo4JTemplate
or leverage Jakarta Data repositories. The integration is smooth, type-safe, and expressive. This approach lets you model complex relationships natively without sacrificing Java idioms or developer productivity.