Beyond Hashing: Building Creative Digest Applications with Quarkus
Learn how Java developers can repurpose SHA and MD5 for colors, cache keys, and consistent hashing using Apache Commons Codec.
Apache Commons Codec is often used for hashing passwords or encoding binary data. But digest algorithms can also solve creative, non-security problems. That can range from deterministic color generation to distributed fingerprinting.
This tutorial shows how to use digest functions in a Quarkus application to generate consistent colors, create GitHub-style identicons, implement content-based cache keys, detect duplicate files, and even build consistent hash routers for distributed systems.
You’ll see how simple hashes can unlock surprising capabilities once you deploy them as REST endpoints.
Prerequisites
Java 17 or newer
Apache Maven 3.9+
Quarkus CLI (optional)
Basic understanding of Quarkus REST endpoints
Project Setup
Create a new Quarkus project:
quarkus create app com.example:creative-digests \
--extension=rest-jackson
cd creative-digestsAdd Apache Commons Codec to your dependencies:
<!-- pom.xml -->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.20.0</version>
</dependency>Now you’re ready to build creative digest utilities inside your Quarkus application. Or you just want to take a look at the final application.
Deterministic Color Generation from Strings
This feature generates consistent colors for usernames, tags, or chart series. Each string maps deterministically to a unique RGB value.
Create a new class ColorService.java under src/main/java/com/example:
package com.example;
import org.apache.commons.codec.digest.DigestUtils;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class ColorService {
public record RGB(int r, int g, int b) {
public String toHex() {
return String.format(”#%02x%02x%02x”, r, g, b);
}
}
public RGB generateColor(String input) {
byte[] hash = DigestUtils.sha256(input);
int r = hash[0] & 0xFF;
int g = hash[1] & 0xFF;
int b = hash[2] & 0xFF;
int max = Math.max(Math.max(r, g), b);
if (max < 128) {
double factor = 200.0 / max;
r = (int) Math.min(255, r * factor);
g = (int) Math.min(255, g * factor);
b = (int) Math.min(255, b * factor);
}
return new RGB(r, g, b);
}
}Expose it through a REST endpoint ColorResource.java: (Rename GreetingResource.java and replace with the following)
package com.example;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
@Path(”/color”)
@Produces(MediaType.APPLICATION_JSON)
public class ColorResource {
@Inject
ColorService colorService;
@GET
public ColorService.RGB color(@QueryParam(”input”) String input) {
return colorService.generateColor(input == null ? “default” : input);
}
}Run the app:
quarkus devThen :
curl "http://localhost:8080/color?input=alice@example.com"You’ll get a deterministic RGB color based on the email string.
{”r”:255,”g”:141,”b”:152}Use cases:
Avatar background colors
Stable category tag colors
Data visualization schemes
Visual Identicons (GitHub-style)
Identicons are small symmetric images derived from hashes. Think of them like a visual fingerprint for users or content. I have written about identicons before but this is a super lightweight way.
Add this generator:
package com.example;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import javax.imageio.ImageIO;
import org.apache.commons.codec.digest.DigestUtils;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class IdenticonService {
private static final int GRID = 5;
private static final int SIZE = 40;
public byte[] generate(String input) {
byte[] hash = DigestUtils.md5(input);
int imageSize = GRID * SIZE;
BufferedImage image = new BufferedImage(imageSize, imageSize, BufferedImage.TYPE_INT_RGB);
Graphics2D g = image.createGraphics();
// background color
g.setColor(Color.WHITE);
g.fillRect(0, 0, imageSize, imageSize);
// foreground color
ColorService.RGB rgb = new ColorService().generateColor(input);
g.setColor(new Color(rgb.r(), rgb.g(), rgb.b()));
int index = 0;
for (int row = 0; row < GRID; row++) {
for (int col = 0; col < (GRID + 1) / 2; col++) {
boolean fill = (hash[index++] & 0xFF) % 2 == 0;
if (fill) {
g.fillRect(col * SIZE, row * SIZE, SIZE, SIZE);
int mirror = GRID - 1 - col;
g.fillRect(mirror * SIZE, row * SIZE, SIZE, SIZE);
}
}
}
g.dispose();
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
ImageIO.write(image, “png”, baos);
return baos.toByteArray();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}Expose the image with a REST endpoint:
package com.example;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
@Path(”/identicon”)
public class IdenticonResource {
@Inject
IdenticonService identiconService;
@GET
@Produces(”image/png”)
public Response get(@QueryParam(”input”) String input) {
byte[] img = identiconService.generate(input == null ? “default” : input);
return Response.ok(img).type(MediaType.valueOf(”image/png”)).build();
}
}Try it out:
curl "http://localhost:8080/identicon?input=alice@example.com" --output test.pngYou’ll see a unique, reproducible image. Perfect for avatars or content previews.
Content-Based Cache Keys
In distributed systems, invalidating cache entries is tricky. A hash-based key automatically changes when any input changes.
package com.example;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.commons.codec.digest.DigestUtils;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class ContentCache {
private final Map<String, String> cache = new ConcurrentHashMap<>();
private String key(String base, Object... parts) {
StringBuilder sb = new StringBuilder(base);
for (Object part : parts)
sb.append(”|”).append(part);
return DigestUtils.sha256Hex(sb.toString());
}
public void put(String base, String value, Object... parts) {
cache.put(key(base, parts), value);
}
public String get(String base, Object... parts) {
return cache.get(key(base, parts));
}
}Example endpoint:
package com.example;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
@Path(”/cache”)
@Produces(MediaType.TEXT_PLAIN)
public class CacheResource {
@Inject
ContentCache cache;
@GET
public String get(@QueryParam(”name”) String name, @QueryParam(”version”) String version) {
String val = cache.get(”page”, name, version);
if (val == null) {
val = “Generated for “ + name + “@” + version;
cache.put(”page”, val, name, version);
return “New entry: “ + val;
}
return “Cache hit: “ + val;
}
}Test:
curl "http://localhost:8080/cache?name=alice&version=1"
curl "http://localhost:8080/cache?name=alice&version=1"
curl "http://localhost:8080/cache?name=alice&version=2"
The second call reuses the cached value; the third triggers a new entry automatically.
File Deduplication
This shows how to detect duplicate files by comparing their SHA-256 digests.
package com.example;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.codec.digest.DigestUtils;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class DeduplicationService {
public Map<String, List<String>> findDuplicates(File directory) throws IOException {
Map<String, List<String>> map = new HashMap<>();
scan(directory, map);
map.entrySet().removeIf(e -> e.getValue().size() < 2);
return map;
}
private void scan(File dir, Map<String, List<String>> map) throws IOException {
File[] files = dir.listFiles();
if (files == null)
return;
for (File f : files) {
if (f.isDirectory())
scan(f, map);
else {
try (InputStream is = new FileInputStream(f)) {
String hash = DigestUtils.sha256Hex(is);
map.computeIfAbsent(hash, k -> new ArrayList<>()).add(f.getAbsolutePath());
}
}
}
}
}Let’s also expose this via a rest endpoint: DeduplicationResource.java
package com.example;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Map;
@Path(”/deduplicate”)
@Produces(MediaType.APPLICATION_JSON)
public class DeduplicationResource {
@Inject
DeduplicationService deduplicationService;
@GET
public Response findDuplicates(@QueryParam(”path”) String path) {
if (path == null || path.trim().isEmpty()) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of(”error”, “Path parameter is required”))
.build();
}
File directory = new File(path);
if (!directory.exists()) {
return Response.status(Response.Status.NOT_FOUND)
.entity(Map.of(”error”, “Directory does not exist: “ + path))
.build();
}
if (!directory.isDirectory()) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of(”error”, “Path is not a directory: “ + path))
.build();
}
try {
Map<String, List<String>> duplicates = deduplicationService.findDuplicates(directory);
if (duplicates.isEmpty()) {
return Response.ok(Map.of(
“message”, “No duplicate files found”,
“scannedPath”, path,
“duplicates”, Map.<String, List<String>>of())).build();
}
// Calculate summary statistics
int totalDuplicateGroups = duplicates.size();
int totalDuplicateFiles = duplicates.values().stream()
.mapToInt(List::size)
.sum();
int totalWastedFiles = totalDuplicateFiles - totalDuplicateGroups; // files that could be removed
return Response.ok(Map.of(
“scannedPath”, path,
“summary”, Map.of(
“duplicateGroups”, totalDuplicateGroups,
“totalDuplicateFiles”, totalDuplicateFiles,
“filesThatCanBeRemoved”, totalWastedFiles),
“duplicates”, duplicates)).build();
} catch (IOException e) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of(”error”, “Error scanning directory: “ + e.getMessage()))
.build();
} catch (SecurityException e) {
return Response.status(Response.Status.FORBIDDEN)
.entity(Map.of(”error”, “Access denied: “ + e.getMessage()))
.build();
}
}
}
and use a test to make sure it is working:
package com.example;
import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.Matchers.greaterThan;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import io.quarkus.test.junit.QuarkusTest;
@QuarkusTest
class DeduplicationResourceTest {
@TempDir
Path tempDir;
@Test
void testFindDuplicates() throws IOException {
// Create test files - some duplicates, some unique
Path file1 = tempDir.resolve(”file1.txt”);
Path file2 = tempDir.resolve(”file2.txt”);
Path file3 = tempDir.resolve(”subdir/file3.txt”);
Path file4 = tempDir.resolve(”unique.txt”);
// Create duplicate content (same hash)
String duplicateContent = “This is duplicate content”;
Files.write(file1, duplicateContent.getBytes());
Files.write(file2, duplicateContent.getBytes());
Files.write(file3, duplicateContent.getBytes());
// Create unique content
Files.write(file4, “This is unique content”.getBytes());
// Call the endpoint
given()
.queryParam(”path”, tempDir.toAbsolutePath().toString())
.when()
.get(”/deduplicate”)
.then()
.statusCode(200)
.body(”scannedPath”, notNullValue())
.body(”summary.duplicateGroups”, greaterThan(0))
.body(”summary.totalDuplicateFiles”, greaterThan(0))
.body(”duplicates”, notNullValue());
}
@Test
void testNoDuplicates() throws IOException {
// Create unique files only
Path file1 = tempDir.resolve(”file1.txt”);
Path file2 = tempDir.resolve(”file2.txt”);
Files.write(file1, “Unique content 1”.getBytes());
Files.write(file2, “Unique content 2”.getBytes());
given()
.queryParam(”path”, tempDir.toAbsolutePath().toString())
.when()
.get(”/deduplicate”)
.then()
.statusCode(200)
.body(”message”, is(”No duplicate files found”));
}
@Test
void testMissingPath() {
given()
.when()
.get(”/deduplicate”)
.then()
.statusCode(400)
.body(”error”, is(”Path parameter is required”));
}
@Test
void testNonExistentDirectory() {
given()
.queryParam(”path”, “/nonexistent/directory/path”)
.when()
.get(”/deduplicate”)
.then()
.statusCode(404)
.body(”error”, containsString(”Directory does not exist”));
}
}I know, it almost looks like I am getting obsessed with tests lately ;-)
Distributed Fingerprints
You can detect data drift across multiple nodes by generating identical fingerprints for identical datasets.
package com.example;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.commons.codec.digest.DigestUtils;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class FingerprintService {
public String datasetFingerprint(List<String> records) {
List<String> sorted = new ArrayList<>(records);
Collections.sort(sorted);
String combined = sorted.stream()
.map(DigestUtils::sha256Hex)
.collect(Collectors.joining());
return DigestUtils.sha256Hex(combined);
}
}Compare fingerprints between nodes: if they differ, the data diverged.
Sharding with Consistent Hashing
Digest functions can distribute keys evenly across servers.
package com.example;
import java.util.Map;
import java.util.TreeMap;
import org.apache.commons.codec.digest.DigestUtils;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class ConsistentRouter {
private final TreeMap<Long, String> ring = new TreeMap<>();
private final int replicas = 150;
public void addServer(String name) {
for (int i = 0; i < replicas; i++) {
long hash = hash(name + i);
ring.put(hash, name);
}
}
public String route(String key) {
if (ring.isEmpty())
return null;
long hash = hash(key);
Map.Entry<Long, String> e = ring.ceilingEntry(hash);
return e != null ? e.getValue() : ring.firstEntry().getValue();
}
private long hash(String s) {
byte[] bytes = DigestUtils.md5(s);
long h = 0;
for (int i = 0; i < 8; i++)
h = (h << 8) | (bytes[i] & 0xFF);
return h;
}
}You can test distribution by routing thousands of keys to three servers and measuring the balance.
Best Practices
MD5 — fast, suitable for non-security uses like visual patterns or routing
SHA-256 — balanced for performance and collision resistance
SHA-512 — stronger, heavier; use when compliance demands it
Performance
For small inputs:
String hash = DigestUtils.sha256Hex(”small”);For large files:
try (InputStream in = new FileInputStream(”large.zip”)) {
String hash = DigestUtils.sha256Hex(in);
}Pitfalls
Hashes are deterministic, not random. Don’t use for true randomness.
Encoding matters. Always use UTF-8 when converting strings.
MD5 is not secure. Never use for passwords.
There is soo much to encode
Apache Commons Codec is far more than a cryptographic utility.
Within a Quarkus application, its digest functions become a creative toolkit for color generation, content fingerprints, distributed verification, and consistent routing.
Every hash is a compact, deterministic fingerprint. A building block for more predictable, elegant systems.
Hash smart, not hard.




