Modern enterprise applications increasingly need persistence technologies that can handle high throughput, flexible data models, distributed workloads, and cloud-native deployment patterns. Relational databases remain an essential part of enterprise Java, but they are not always the natural fit for every workload. Session data, caching, feature flags, shopping carts, tokens, configuration objects, rate-limiting information, and other lookup-oriented data can often be represented efficiently as key-value records.
The challenge is that introducing a NoSQL database into an established Java enterprise environment can create another challenge: vendor-specific APIs. If application code becomes tightly coupled to a particular database client, migrating to another database later can require substantial refactoring.
Eclipse JNoSQL provides an approach to this problem by giving Java developers APIs and mapping facilities for working with different NoSQL database models. Its key-value support is particularly useful when an application needs simple, fast access based on a unique key while still benefiting from Java’s familiar dependency injection, object mapping, and enterprise programming model.
This article explores how to introduce a key-value database into an enterprise Java application using Eclipse JNoSQL, with an emphasis on portable APIs, flexible persistence, clean architecture, testing, and practical coding examples.
Understanding the Key-Value Database Model
A key-value database stores information as a collection of keys associated with values.
Conceptually, the model is straightforward:
key → valueFor example:
"user:1001" → customer information
"session:8a72..." → session information
"feature:checkout-v2" → feature configuration
"cart:customer-1001" → shopping cartThe key is normally unique, and the value represents the associated data. Depending on the database, the value can be a string, binary representation, JSON-like structure, or another supported data representation.
The simplicity of the model makes key-value databases particularly attractive for workloads dominated by operations such as:
get(key)
put(key, value)
delete(key)This differs considerably from a relational database, where developers commonly think in terms of tables, rows, relationships, joins, and SQL queries.
That difference is important when designing an enterprise system. A key-value database should not simply be treated as a replacement for a relational database. Instead, it should normally be introduced for workloads that naturally fit key-based access.
For example, suppose an application needs to retrieve a user’s active session:
session:123456If the application almost always knows the session identifier before retrieving the data, a key-value model is a natural match.
Eclipse JNoSQL provides APIs designed to reduce coupling between application code and the underlying NoSQL technology. The broader Jakarta NoSQL model separates communication APIs from mapping APIs, allowing different database types and implementations to participate in a common Java programming model.
Why Portability Matters in Enterprise Java
Enterprise applications often live for many years. A technology selected today may eventually need to be replaced because of licensing, infrastructure, operational requirements, performance characteristics, cloud strategy, organizational standards, or changes in the application’s workload.
A database-specific client API can make such a migration expensive.
For example, an application might directly depend on a vendor client:
VendorClient client = VendorClient.connect(configuration);
client.put("customer:1001", customerJson);This code may be perfectly functional, but the persistence layer now knows about VendorClient.
If another database is introduced, the application may need to change:
AnotherVendorClient client = AnotherVendorClient.connect(configuration);The problem becomes more significant when database-specific types spread through business services, REST controllers, scheduled jobs, and tests.
A portable abstraction changes the architecture:
REST/API Layer
|
v
Application Service
|
v
Repository / Persistence API
|
v
Eclipse JNoSQL
|
v
Key-Value Provider
|
v
DatabaseThe business layer does not need to understand the underlying database’s proprietary client API.
Eclipse JNoSQL was designed around this broader goal of providing common APIs for NoSQL database categories while keeping database-specific implementations behind the abstraction. Its project scope includes APIs for key-value, document, column, and graph databases.
Choosing the Right Enterprise Workload
Before introducing JNoSQL, identify the workload that benefits from key-value persistence.
Good candidates typically have a clearly identifiable key and do not require complex relational querying.
Examples include:
- Authentication and session tokens
- Shopping carts
- User preferences
- Temporary workflow state
- Feature flags
- API idempotency keys
- Distributed application state
- Cached representations
- Device state
- Short-lived authorization information
- Lookup tables
- Rate-limiting state
Consider an API idempotency key:
idempotency:payment:7f8c1aThe application needs to answer a simple question:
Has this request already been processed?
A key-value store can represent that requirement naturally.
By contrast, a domain involving extensive joins, complex reporting, referential integrity, and highly relational transactions may still belong in a relational database.
The goal is therefore not “replace SQL with NoSQL.” The goal is polyglot persistence: use the persistence model that fits each workload while keeping the application’s architecture understandable.
Setting Up an Enterprise Java Project
A typical Maven-based application can organize the persistence layer around JNoSQL and the provider implementation required by the chosen database.
A conceptual Maven setup might look like this:
<dependencies>
<dependency>
<groupId>jakarta.nosql</groupId>
<artifactId>jakarta.nosql-api</artifactId>
<version>${jnosql.version}</version>
</dependency>
<!-- Add the JNoSQL provider/integration required
by the selected key-value database. -->
</dependencies>The exact provider dependency depends on the database being used and the JNoSQL release selected by the project.
For an enterprise application, it is generally preferable to keep versions centralized:
<properties>
<java.version>21</java.version>
<jnosql.version>1.1.18</jnosql.version>
</properties>The specific version should be aligned with the provider, Jakarta EE runtime, Java version, and organization’s dependency-management policy.
The important architectural principle is that application code should depend primarily on the portable APIs rather than directly on a database’s client library.
Modeling a Key-Value Entity
Suppose an enterprise application needs to store customer preferences.
A Java representation could be:
import jakarta.nosql.mapping.Column;
import jakarta.nosql.mapping.Entity;
import jakarta.nosql.mapping.Id;
@Entity
public class CustomerPreferences {
@Id
private String customerId;
@Column
private String language;
@Column
private boolean emailNotifications;
@Column
private String theme;
public CustomerPreferences() {
}
public CustomerPreferences(
String customerId,
String language,
boolean emailNotifications,
String theme) {
this.customerId = customerId;
this.language = language;
this.emailNotifications = emailNotifications;
this.theme = theme;
}
public String getCustomerId() {
return customerId;
}
public String getLanguage() {
return language;
}
public boolean isEmailNotifications() {
return emailNotifications;
}
public String getTheme() {
return theme;
}
}The entity contains the application-facing representation of the data. The persistence infrastructure is responsible for mapping that object to the underlying NoSQL representation.
This is one of the useful characteristics of the JNoSQL approach: application developers can work with Java objects rather than repeatedly converting domain objects to vendor-specific representations.
The mapping API also follows an annotation-oriented model familiar to Java enterprise developers.
Using a Repository-Oriented Design
For an enterprise application, it is useful to place persistence operations behind a repository boundary.
For example:
public interface CustomerPreferencesRepository {
CustomerPreferences save(CustomerPreferences preferences);
Optional<CustomerPreferences> findById(String customerId);
void deleteById(String customerId);
}The business service then depends on this abstraction rather than on a database client:
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
@ApplicationScoped
public class CustomerPreferencesService {
private final CustomerPreferencesRepository repository;
@Inject
public CustomerPreferencesService(
CustomerPreferencesRepository repository) {
this.repository = repository;
}
public CustomerPreferences update(
String customerId,
String language,
boolean notifications,
String theme) {
CustomerPreferences preferences =
new CustomerPreferences(
customerId,
language,
notifications,
theme);
return repository.save(preferences);
}
public Optional<CustomerPreferences> get(String customerId) {
return repository.findById(customerId);
}
}This separation is valuable even when JNoSQL itself provides convenient repository abstractions.
The application service expresses business behavior. The repository expresses persistence behavior. JNoSQL handles the communication and mapping between the application and the NoSQL implementation.
Introducing Jakarta Data Repositories
Modern JNoSQL versions also work with the Jakarta Data programming model, providing repository-style abstractions that can remove much of the repetitive persistence code.
A conceptual repository can look like this:
import jakarta.data.repository.CrudRepository;
import jakarta.data.repository.Repository;
@Repository
public interface CustomerPreferencesRepository
extends CrudRepository<CustomerPreferences, String> {
}The exact repository interfaces and supported operations should be checked against the JNoSQL and Jakarta Data versions used by the application, but the architectural idea is straightforward: describe persistence operations declaratively rather than writing database-client boilerplate for every CRUD operation.
This can substantially reduce code in an enterprise application.
Instead of repeatedly writing:
database.get(...);
database.put(...);
database.delete(...);developers can express the persistence contract at a higher level.
Working with JNoSQL’s Template API
When an application needs more explicit persistence operations, JNoSQL’s Template API provides a useful alternative.
For example:
import jakarta.inject.Inject;
import jakarta.nosql.Template;
public class PreferencesStore {
@Inject
private Template template;
public CustomerPreferences save(
CustomerPreferences preferences) {
return template.insert(preferences);
}
public Optional<CustomerPreferences> find(
String customerId) {
return template.find(
CustomerPreferences.class,
customerId);
}
public void delete(String customerId) {
template.delete(
CustomerPreferences.class,
customerId);
}
}The current Template API includes operations for inserting, finding, and deleting entities, as well as query-related operations where the underlying database supports them.
For a key-value database, however, it is important not to assume that every query operation available in a general NoSQL API will have the same meaning or performance characteristics as it would in a relational database.
Key-value databases are fundamentally optimized around key-based access.
Designing Keys Carefully
One of the most important decisions in a key-value system is key design.
Consider:
1001
1002
1003These keys are technically valid, but they do not communicate much about their purpose.
A more explicit design might use:
customer-preferences:1001
customer-preferences:1002
customer-preferences:1003This creates a logical namespace.
Another example is a shopping cart:
cart:customer:1001Or a session:
session:8f9d2cKey design should be consistent across the organization.
A common approach is:
<entity>:<identifier>For example:
order:82913
customer:1001
session:abc123For more complex systems, a hierarchy can be useful:
tenant:acme:customer:1001
tenant:acme:cart:1001
tenant:globex:customer:1001This can help isolate tenants and make operational management easier.
The key is not simply an implementation detail. In a key-value database, it is part of the application’s data model.
Managing Expiration and Temporary Data
Many key-value workloads contain information that should eventually disappear.
A session token, for example, may only be valid for 30 minutes.
JNoSQL’s Template API includes support for time-to-live in persistence operations where the underlying database supports that capability.
A conceptual example is:
Duration ttl = Duration.ofMinutes(30);
SessionToken token =
new SessionToken(
"abc123",
"user-42",
Instant.now());
template.insert(token, ttl);This is particularly useful for temporary state.
However, expiration should not be treated as a substitute for application-level security. If a token expires in the database, the application should still validate authorization, token integrity, and other security requirements.
It is also important to understand that TTL support is database-dependent. If the underlying provider does not support TTL, the behavior may differ from a database that provides native expiration.
Keeping the Application Portable
Portability does not mean pretending that all databases behave identically.
A Redis-like key-value system, a distributed cache, and another key-value database can have very different capabilities around:
- Transactions
- Consistency
- Expiration
- Atomic operations
- Serialization
- Replication
- Partitioning
- Secondary indexes
- Query capabilities
- Concurrency
- Availability guarantees
JNoSQL can abstract common programming concepts, but it cannot eliminate fundamental differences between database technologies.
Therefore, portable application architecture should distinguish between two kinds of behavior.
The first is application-level persistence:
repository.findById(customerId);
repository.save(preferences);The second is provider-specific optimization:
// Example conceptual optimization:
// atomic increment, database-specific command, etc.Provider-specific functionality should remain isolated.
If an application exposes vendor-specific operations throughout the service layer, it loses much of the benefit of portability.
A better design is:
Application
|
+---- Portable repository operations
|
+---- Optional provider-specific adapter
|
+---- Database-specific featureThis allows the majority of the application to remain portable while still allowing specialized capabilities when they provide genuine business value.
Integrating with Jakarta EE
Eclipse JNoSQL fits naturally into an enterprise Java architecture using technologies such as CDI.
For example:
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class CustomerService {
private final CustomerPreferencesRepository repository;
public CustomerService(
CustomerPreferencesRepository repository) {
this.repository = repository;
}
public Optional<CustomerPreferences> preferences(
String customerId) {
return repository.findById(customerId);
}
}The service can then be consumed by a REST endpoint:
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.core.Response;
@Path("/customers")
public class CustomerResource {
@Inject
CustomerService service;
@GET
@Path("/{id}/preferences")
public Response getPreferences(
@PathParam("id") String id) {
return service.preferences(id)
.map(Response::ok)
.map(Response.ResponseBuilder::build)
.orElseGet(() ->
Response.status(
Response.Status.NOT_FOUND).build());
}
}The REST layer does not know which key-value database is being used.
That separation is exactly what an enterprise architecture should aim for.
Testing Without a Database Dependency in Every Test
One of the advantages of introducing a repository abstraction is that business services can be tested without starting the actual database.
A simple test double could be:
public class InMemoryPreferencesRepository
implements CustomerPreferencesRepository {
private final Map<String, CustomerPreferences> data =
new HashMap<>();
@Override
public CustomerPreferences save(
CustomerPreferences preferences) {
data.put(
preferences.getCustomerId(),
preferences);
return preferences;
}
@Override
public Optional<CustomerPreferences> findById(
String customerId) {
return Optional.ofNullable(data.get(customerId));
}
@Override
public void deleteById(String customerId) {
data.remove(customerId);
}
}A service test can then concentrate on business rules.
For example:
@Test
void shouldStoreCustomerPreferences() {
CustomerPreferencesRepository repository =
new InMemoryPreferencesRepository();
CustomerPreferencesService service =
new CustomerPreferencesService(repository);
service.update(
"1001",
"en",
true,
"dark");
assertTrue(
service.get("1001").isPresent());
}Integration tests should still exercise the actual JNoSQL provider and database because the abstraction cannot guarantee that provider-specific configuration, serialization, consistency, or operational behavior is correct.
A strong test strategy therefore contains both unit tests and integration tests.
Handling Transactions and Consistency
One of the most important differences between relational and key-value persistence concerns transactional behavior.
Enterprise developers accustomed to relational transactions should not automatically assume that every operation against a key-value database has equivalent transactional semantics.
Consider:
preferencesRepository.save(preferences);
auditRepository.save(auditRecord);If those operations involve separate persistence systems, there may not be a single transaction covering both.
The application therefore needs to understand the consistency model of the selected database and design workflows accordingly.
For example, some operations can be made idempotent:
public void processPayment(String requestId) {
if (alreadyProcessed(requestId)) {
return;
}
processPaymentOperation();
markAsProcessed(requestId);
}Idempotency can be particularly useful in distributed systems where requests may be retried.
The persistence technology is only one part of the solution. Correct enterprise architecture requires the application to understand failure, retry, consistency, and concurrency behavior.
Observability and Operations
Introducing a new database into production also introduces a new operational dependency.
The application should monitor at least:
- Request latency
- Error rates
- Connection failures
- Timeouts
- Read/write throughput
- Database availability
- Resource utilization
- Expired records where relevant
- Retry activity
- Serialization failures
A repository abstraction makes instrumentation easier because the application has a clearly defined persistence boundary.
For example, metrics can be associated with operations such as:
preferences.read
preferences.write
preferences.deleteRather than exposing database-specific concepts throughout the business layer, the application can monitor business-oriented persistence operations.
Logging should similarly avoid recording sensitive values.
A log message such as:
Loading preferences for customer 1001may be useful.
Logging the complete serialized customer record may expose unnecessary personal or confidential information.
Migration from a Relational Database
Introducing a key-value database does not necessarily mean performing a complete migration from an existing relational system.
A safer enterprise strategy is often incremental.
Start with one bounded workload:
Existing Java Application
|
+---- Relational Database
|
+---- Key-Value Database
|
+---- Session StateFor example, session data can be moved first.
The relational database remains responsible for durable business entities:
Customer
Order
Invoice
Product
PaymentThe key-value database handles:
Session
Temporary Token
Cache Entry
Short-Lived Workflow StateThis arrangement allows the organization to gain experience with operational characteristics without redesigning the entire persistence architecture.
Over time, additional workloads can be evaluated individually.
Avoiding the “Database Abstraction” Trap
There is a subtle architectural danger in pursuing portability too aggressively.
An abstraction can become so generic that it hides important database semantics.
For example:
repository.queryAnything(...)may look flexible, but it can make the application difficult to understand and can encourage developers to treat every database as if it were relational.
A better approach is to keep the abstraction aligned with the actual business operations.
Instead of:
executeArbitraryQuery(...)prefer:
findSession(sessionId);
saveSession(session);
deleteSession(sessionId);This creates a domain-oriented persistence API.
The database remains an implementation detail, while the application exposes meaningful operations.
A Practical Enterprise Architecture
A mature architecture might eventually look like this:
┌─────────────────────┐
│ REST / UI │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ Application Service │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ Domain Repository │
│ Interface │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ Eclipse JNoSQL │
│ Mapping / Repository│
│ / Template │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ Key-Value Store │
└─────────────────────┘This architecture provides several useful boundaries.
The presentation layer knows nothing about the database.
The application service knows the business use case but not the database protocol.
The repository describes persistence behavior.
JNoSQL provides the portable NoSQL programming model.
The provider connects that model to the selected key-value database.
This structure also makes it easier to evolve the system.
A Complete Small Example
Putting the concepts together, a simplified application can contain an entity:
@Entity
public class Session {
@Id
private String id;
@Column
private String userId;
@Column
private Instant createdAt;
public Session() {
}
public Session(
String id,
String userId,
Instant createdAt) {
this.id = id;
this.userId = userId;
this.createdAt = createdAt;
}
public String getId() {
return id;
}
public String getUserId() {
return userId;
}
public Instant getCreatedAt() {
return createdAt;
}
}A repository can expose the required operations:
@Repository
public interface SessionRepository
extends CrudRepository<Session, String> {
}A service can consume it:
@ApplicationScoped
public class SessionService {
private final SessionRepository repository;
@Inject
public SessionService(SessionRepository repository) {
this.repository = repository;
}
public Session create(String id, String userId) {
Session session =
new Session(
id,
userId,
Instant.now());
return repository.save(session);
}
public Optional<Session> find(String id) {
return repository.findById(id);
}
public void remove(String id) {
repository.deleteById(id);
}
}And a REST resource can expose the functionality:
@Path("/sessions")
public class SessionResource {
@Inject
SessionService service;
@GET
@Path("/{id}")
public Response find(
@PathParam("id") String id) {
return service.find(id)
.map(Response::ok)
.map(Response.ResponseBuilder::build)
.orElseGet(() ->
Response.status(404).build());
}
}The exact repository API and dependency versions should be aligned with the JNoSQL/Jakarta Data combination selected by the project, but the architectural pattern remains the same: domain code depends on a stable persistence abstraction rather than a proprietary database API.
A Step-by-Step Adoption Strategy
For an organization introducing key-value persistence for the first time, a phased approach is useful.
Identify the workload
Find an existing workload dominated by key-based reads and writes.
Define the domain boundary
Create a repository interface representing what the application actually needs.
Model the entity
Use JNoSQL mapping annotations and a Java domain model.
Introduce JNoSQL
Configure the appropriate JNoSQL provider and runtime integration.
Build integration tests
Test real persistence behavior against the selected database.
Add observability
Measure latency, errors, throughput, and resource consumption.
Run a controlled production workload
Start with a bounded feature rather than moving an entire application.
Evaluate the result
Compare actual operational behavior with the original requirements.
Expand selectively
Move additional workloads only when their access patterns justify key-value persistence.
This approach reduces risk because the organization learns how the technology behaves before making it a foundational dependency.
Common Mistakes to Avoid
One common mistake is choosing a key-value database simply because NoSQL is perceived as more scalable.
Technology selection should begin with access patterns and business requirements.
Another mistake is storing everything as an opaque blob:
key → giant JSON documentAlthough this can work, it may make evolution, validation, partial updates, and operational troubleshooting more difficult.
Another mistake is assuming that JNoSQL makes every database interchangeable.
Portable APIs reduce coupling, but they cannot make fundamentally different consistency and capability models identical.
Finally, teams should avoid exposing database-specific objects throughout the application:
VendorSpecificConnection
VendorSpecificCommand
VendorSpecificResultThose types belong, where necessary, behind the persistence boundary.
The business layer should deal with domain objects and business operations.
The Enterprise Value of Flexible Persistence
The real value of Eclipse JNoSQL is not simply that it lets a Java application communicate with a NoSQL database.
Its larger architectural value is that it gives an enterprise Java application a structured way to introduce NoSQL persistence without forcing the entire application to become dependent on one database vendor’s programming model.
The distinction is important.
A database can be changed.
An application’s architecture should not have to be rebuilt every time the persistence implementation changes.
JNoSQL’s mapping model, communication APIs, repository-oriented approaches, and integration with the broader Jakarta ecosystem can help establish that separation.
For developers already familiar with Jakarta EE concepts such as CDI, dependency injection, annotations, REST services, and repository abstractions, the learning curve can also be reduced because the programming style remains recognizably Java enterprise-oriented.
Conclusion
Introducing a key-value database into an enterprise Java environment is fundamentally an architectural decision rather than simply a database installation exercise. The organization must decide which workloads actually benefit from key-based persistence, how those workloads fit alongside existing relational data, how consistency and transactions will be handled, and how the system will remain maintainable as infrastructure evolves.
Eclipse JNoSQL provides an important piece of that architecture by establishing a Java-oriented programming model for NoSQL persistence. Instead of allowing every application service to communicate directly with a proprietary database client, an enterprise can place a portable persistence layer between the business application and the underlying database.
That separation has practical consequences.
A service can work with a repository rather than a database connection. A domain entity can be mapped using Java annotations rather than manually serialized for every operation. A test can replace the persistence implementation when testing business logic. An organization can evaluate another compatible persistence implementation without automatically rewriting every service that uses the data.
At the same time, portability should be understood realistically. JNoSQL does not erase the differences between key-value databases. Database-specific behavior around consistency, transactions, expiration, replication, partitioning, atomic operations, querying, and failure handling still matters. Enterprise teams should therefore use the portable API for common persistence behavior while keeping genuinely provider-specific functionality isolated behind well-defined boundaries.
The most effective implementation is usually incremental. Rather than attempting to replace an existing relational database, an organization can begin with a narrowly defined workload such as session state, temporary tokens, feature configuration, shopping carts, or idempotency records. That workload can be modeled as a Java entity, exposed through a repository, persisted through JNoSQL, tested against the real provider, and monitored in production. Once the operational characteristics are understood, other workloads can be evaluated independently.
Key design decisions should remain visible throughout the process. Keys should be predictable and consistently namespaced. Expiration should be deliberately designed for temporary information. Security-sensitive data should be protected regardless of database technology. Consistency requirements should be documented rather than assumed. Observability should be built into the persistence boundary. And application services should remain focused on business behavior instead of database-specific commands.
The combination of a domain-oriented repository, JNoSQL’s portable APIs, Java object mapping, Jakarta EE integration, and a carefully selected key-value database can therefore provide a practical path toward polyglot persistence. The result is not simply another database attached to an enterprise application. Done correctly, it is a persistence architecture in which different data workloads can use different storage models while the application remains organized around stable business concepts.
That is ultimately the strongest reason to introduce key-value persistence through a portable API: the database becomes an implementation choice within a larger architecture rather than becoming the architecture itself. Enterprise Java applications can take advantage of the speed, flexibility, and distributed characteristics of key-value storage while retaining clear application boundaries and reducing unnecessary dependence on vendor-specific programming models. Eclipse JNoSQL can serve as the bridge between those two worlds, allowing teams to adopt NoSQL persistence incrementally, keep their Java code focused on business concerns, and retain greater flexibility as their enterprise systems evolve.