Caching with Dragonfly and Java in 5 Minutes
Boost Java app performance with Dragonfly as a Redis-compatible cache. Learn to integrate it with Spring Boot for scalable caching.
July 31, 2025

Introduction: The Need for Speed without the Lock-In
In modern software development, caching is often the difference between a sluggish application and one that feels instantaneous. While Redis has long been the go-to choice for in-memory data stores, newer solutions like Dragonfly offer compelling alternatives with the same API and more powerful architecture without forcing developers to rethink their entire stack. The best tools strike a balance: they deliver better performance while respecting familiar paradigms. In this guide, we’ll explore how Dragonfly, with its Redis compatibility, slots seamlessly into Java applications, offering speed, scalability, and, critically, the ability to migrate or innovate without starting from scratch.
The Tooling Stack
Our project relies on a few key technologies as described below.
Spring Boot is the de facto choice for Java application development for many teams, providing a convention-over-configuration approach that eliminates boilerplate while maintaining flexibility. Spring Boot’s auto-configuration and starter dependencies represent a mature ecosystem where standards have evolved through community consensus and real-world usage.
Spring Data JPA provides a standardized abstraction over database operations, allowing developers to work with entities rather than raw SQL. This abstraction layer means you can switch between different database providers (PostgreSQL, MySQL, etc.) with minimal code changes, demonstrating the value of standardized interfaces.
Spring Cache Abstraction offers a standardized caching abstraction that works with multiple cache providers. The beauty of this approach is that your caching logic remains the same regardless of the choice of your cache implementation.
Dragonfly is a fully Redis-compatible, multi-threaded, high-performance in-memory data store built for the most demanding workloads. The Redis serialization protocol (RESP) represents one of those cases where a popular API becomes the de facto standard. Dragonfly honors this reality by maintaining full Redis API compatibility while completely rearchitecting the underlying engine. The result is a drop-in replacement that requires zero code changes but delivers dramatically better performance through its modern multi-threaded, shared-nothing architecture.
Last but not least, we will pair one of the most advanced open-source relational databases, PostgreSQL, with Spring Data JPA to build the storage layer. We will also utilize development tools like Docker and Maven, along with other libraries within the Java ecosystem.
Building a Dictionary API with Caching
A dictionary API can be a great example demonstrating the power of a caching layer in the real world. While simple in concept, its traffic patterns create unique challenges. For instance, popular words can generate thousands of lookups in minutes when the API is shared across multiple applications. So even in this straightforward system, a proper caching layer can reduce database load by orders of magnitude while improving response times and system stability.
Now let’s walk through the dictionary API we are about to build. If you want to follow along, all the code snippets in this tutorial are available in our example repository.
Prerequisites
Before we begin, ensure you have the following tools installed:
- Java 17+: We use Java 24 for this project, but any version 17 or higher will work.
- Maven: A popular build tool for Java projects, used for dependency management and building.
- Docker: A platform used to develop, ship, and run applications inside containers. A container is a lightweight, standalone executable that includes everything needed to run a piece of software—code, runtime, system tools, libraries, and settings.
Running PostgreSQL and Dragonfly
First, let’s make sure we have PostgreSQL and Dragonfly server instances running locally in Docker.
# docker-compose.yaml
services:
dragonfly:
image: docker.dragonflydb.io/dragonflydb/dragonfly
container_name: dictionary-app-dragonfly
ports:
- "6380:6379"
postgres:
image: postgres:17
container_name: dictionary-app-postgres
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: dictionarydb
ports:
- "5432:5432"
volumes:
- ./db-init:/docker-entrypoint-initdb.d
Start the services:
$> docker-compose up -d
This will start both services. The PostgreSQL database will be automatically seeded with initial dictionary data using the db-init/init.sql
script.
-- db-init/init.sql
CREATE TABLE IF NOT EXISTS dictionary_entry (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
word VARCHAR(255) UNIQUE NOT NULL,
definition TEXT NOT NULL,
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
INSERT INTO dictionary_entry(word, definition, updated_at) VALUES
('cache', 'A component that transparently stores data for faster retrieval.', NOW()),
('dragonfly', 'A fully Redis-compatible, high-performance in-memory data store.', NOW()),
('java', 'A versatile, object-oriented programming language widely used for building enterprise applications.', NOW()),
('spring', 'An opinionated framework simplifying the development of Java-based applications.', NOW()),
('redis', 'An open-source, in-memory data structure store used as a cache and message broker.', NOW()),
('database', 'An organized collection of structured data typically stored electronically.', NOW()),
('docker', 'A platform used to build, ship, and run software inside lightweight, portable containers.', NOW()),
('api', 'A set of definitions and protocols for building and integrating application software.', NOW()),
('json', 'A lightweight data-interchange format that is easy for humans to read and write.', NOW()),
('yaml', 'A human-friendly, serialization language commonly used for configuration files.', NOW());
Create a Spring Boot Project
To set up our Spring Boot project, we’ll use Spring Initializr. Set your project details as shown below, choose the dependencies listed, then click “Generate” to download your starter project.

Spring Initializr | A Quickstart Generator for Spring Projects
Here’s what the project structure will look like:
src/
├── main/
│ ├── java/
│ │ └── com/
│ │ └── dragonfly/
│ │ └── dictionary/
│ │ ├── DictionaryApplication.java
│ │ ├── controller/
│ │ │ └── DictionaryController.java
│ │ ├── model/
│ │ │ ├── DictionaryEntry.java
│ │ │ └── DictionaryResponse.java
│ │ ├── repository/
│ │ │ └── DictionaryRepository.java
│ │ └── service/
│ │ └── DictionaryService.java
│ └── resources/
│ └── application.yml
Service Connections Setup
Before actually running our application server, let’s take a look at a few code snippets to have a better understanding. The code below initializes the database and cache connections for our dictionary service.
// src/main/java/com/dragonfly/dictionary/DictionaryApplication.java
package com.dragonfly.dictionary;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication
@EnableCaching
public class DictionaryApplication {
public static void main(String[] args) {
SpringApplication.run(DictionaryApplication.class, args);
}
}
# src/main/resources/application.yml
spring:
datasource:
url: jdbc:postgresql://localhost:5432/dictionarydb
username: postgres
password: postgres
jpa:
hibernate:
ddl-auto: update
data:
# Using the Redis client to connect to and manipulate data in Dragonfly.
# By using Docker, Dragonfly is running locally on port 6380.
redis:
host: localhost
port: 6380
server:
port: 8080
First, we enable caching in our Spring Boot application using the @EnableCaching
annotation. The application configuration connects to PostgreSQL for persistent storage and Dragonfly for caching. Note that we are using the Redis client here talking to our Dragonfly server running on port 6380
. For production deployments, you would replace these localhost URLs with your actual service endpoints, ideally fetched from a secure location, while keeping the same connection logic.
Database Schema
The code below defines our dictionary_entry
table schema using JPA annotations. Note that you can also pluralize or customize your table names by using the @Table(name = "XXX")
annotation. In this example, we keep it simple with the default. Each dictionary record contains a UUID primary key, the word itself, and its definition. The timestamps track when the entry was last updated. The @Column
constraints ensure data integrity by preventing null values in all fields.
// src/main/java/com/dragonfly/dictionary/model/DictionaryEntry.java
package com.dragonfly.dictionary.model;
import jakarta.persistence.*;
import java.time.LocalDateTime;
import java.util.UUID;
@Entity
public class DictionaryEntry {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
@Column(unique = true, nullable = false)
private String word;
@Column(nullable = false, length = 4096)
private String definition;
@Column(nullable = false)
private LocalDateTime updatedAt = LocalDateTime.now();
// Constructors.
public DictionaryEntry() {}
public DictionaryEntry(String word, String definition) {
this.word = word;
this.definition = definition;
}
// Getters and setters.
public UUID getId() {
return id;
}
public void setId(UUID id) {
this.id = id;
}
public String getWord() {
return word;
}
public void setWord(String word) {
this.word = word;
}
public String getDefinition() {
return definition;
}
public void setDefinition(String definition) {
this.definition = definition;
}
public LocalDateTime getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(LocalDateTime updatedAt) {
this.updatedAt = updatedAt;
}
}
A few deliberate design choices warrant explanation: The word
field uses a unique constraint, ensuring no duplicate words can exist in our dictionary. For the definition
field, we use a 4096-character limit, which should cover most dictionary definitions. The updatedAt
timestamp helps track when entries were last modified, useful for cache invalidation strategies.
Response Model
We also have a response model that includes cache details. The cached
field will help us easily verify if our caching layer is working correctly when we run the application later.
// src/main/java/com/dragonfly/dictionary/model/DictionaryResponse.java
package com.dragonfly.dictionary.model;
public class DictionaryResponse {
private String word;
private String definition;
private boolean cached;
public DictionaryResponse() {}
public DictionaryResponse(String word, String definition, boolean cached) {
this.word = word;
this.definition = definition;
this.cached = cached;
}
// Getters and setters.
public String getWord() {
return word;
}
public void setWord(String word) {
this.word = word;
}
public String getDefinition() {
return definition;
}
public void setDefinition(String definition) {
this.definition = definition;
}
public boolean isCached() {
return cached;
}
public void setCached(boolean cached) {
this.cached = cached;
}
}
Repository Layer
The code below creates the repository interface using Spring Data JPA. This provides a type-safe abstraction over database operations, allowing us to work with entities rather than raw SQL:
// src/main/java/com/dragonfly/dictionary/repository/DictionaryRepository.java
package com.dragonfly.dictionary.repository;
import com.dragonfly.dictionary.model.DictionaryEntry;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.UUID;
import java.util.Optional;
public interface DictionaryRepository extends JpaRepository<DictionaryEntry, UUID> {
Optional<DictionaryEntry> findByWordIgnoreCase(String word);
}
This repository interface extends JpaRepository
, which provides standard CRUD operations. The findByWordIgnoreCase
method demonstrates Spring Data JPA’s query method feature, automatically generating a case-insensitive query based on the method name.
Backend Server API
Now we have both the database and repository layer defined. Let’s look at the code for the API endpoint for our Spring Boot server. The endpoint below takes in a word and returns its definition with cache information. It showcases a complete cache-first optimization flow for dictionary lookups.
The handler below implements the lookup logic with cache-first optimization. When a request hits our dictionary endpoint (e.g., /dictionary/cache
), it first checks the caching layer using the word as the key. If cached (hot path), it returns the definition immediately. On cache misses (cold path), it falls back to PostgreSQL. Whenever there’s a cache miss, it reloads the word back into the cache. This ensures commonly requested words are quickly available again. Many backend systems use this two-layered approach, combining the fast responses of caching with the reliability of a database. This setup helps maintain consistent performance and low response times for popular words.
// src/main/java/com/dragonfly/dictionary/service/DictionaryService.java
package com.dragonfly.dictionary.service;
import com.dragonfly.dictionary.repository.DictionaryRepository;
import com.dragonfly.dictionary.model.DictionaryResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Service;
@Service
public class DictionaryService {
@Autowired
private DictionaryRepository dictionaryRepository;
@Autowired
private CacheManager cacheManager;
public DictionaryResponse getDefinitionWithCacheInfo(String word) {
String cacheKey = word.toLowerCase();
Cache cache = cacheManager.getCache("dictionaryCache");
boolean cached = false;
String definition = null;
if (cache != null && cache.get(cacheKey) != null) {
definition = (String) cache.get(cacheKey).get();
cached = true;
} else {
// Cache miss: read from database, and then cache the record
definition = dictionaryRepository.findByWordIgnoreCase(word)
.map(entry -> entry.getDefinition())
.orElse("Word not found.");
if (cache != null) {
cache.put(cacheKey, definition);
}
}
return new DictionaryResponse(word, definition, cached);
}
}
Controller Layer
And finally, here’s the simple code for our REST controller:
// src/main/java/com/dragonfly/dictionary/controller/DictionaryController.java
package com.dragonfly.dictionary.controller;
import com.dragonfly.dictionary.service.DictionaryService;
import com.dragonfly.dictionary.model.DictionaryResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/dictionary")
public class DictionaryController {
@Autowired
private DictionaryService dictionaryService;
@GetMapping("/{word}")
public DictionaryResponse getDefinition(@PathVariable String word) {
return dictionaryService.getDefinitionWithCacheInfo(word);
}
}
Running the Backend Server
Now that you understand all the different parts of the application, it’s finally time to try it out!
Run mvn clean install
to install dependencies, then start the server with mvn spring-boot:run
. Your app will launch at http://localhost:8080
.
Testing the API
Let’s test our dictionary API by making a request. Since this is the first request, it will retrieve the definition directly from the database.
$> curl http://localhost:8080/dictionary/dragonfly
You’ll get this response:
{
"word": "dragonfly",
"definition": "A fully Redis-compatible, high-performance in-memory data store.",
"cached": false
}
Now, let’s repeat the request for the same word to check if it’s served from the cache:
$> curl http://localhost:8080/dictionary/cache
The response this time:
{
"word": "dragonfly",
"definition": "A fully Redis-compatible, high-performance in-memory data store.",
"cached": true
}
Notice how the cached value changes from false
to true
after the first request, indicating the response is now served directly from the cache.
Inspecting the Cache
You can also directly inspect your cache using Redis CLI.
# Connect to Dragonfly:
$> redis-cli -p 6380
# Inspect cached keys in Dragonfly:
dragonfly$> SCAN 0
1) "0"
2) 1) "dictionaryCache::dragonfly"
dragonfly$> GET dictionaryCache::dragonfly
"\xac\xed\x00\x05t\x00@A fully Redis-compatible, high-performance in-memory data store."
This confirms our dictionary API is correctly caching common lookups to ensure faster responses.
Same API, More Performance
At Dragonfly, the team embraces the same philosophy as many robust Java projects do: we don’t break compatibility for the sake of change. By preserving the Redis API while rebuilding the underneath architecture for modern multi-core hardware, Dragonfly delivers drop-in compatibility with both vertical (multi-core) and horizontal (multi-node) scalability, without forcing developers to rewrite their apps.
Ready to experience the difference? Get started with Dragonfly locally, or deploy a fully managed data store on Dragonfly Cloud, which will provision and be ready to use in under 5 minutes as well!