We’ll build a small HTTP service that registers and retrieves blob metadata stored in MinIO.
The raw bytes live in MinIO; FoundationDB stores the metadata catalog — owner/tenant, bucket, object key, size, hash, timestamps, tags, etc.
Goal: a minimal MVP to later iterate toward advanced idempotency, presigned URLs, and composition with other SNAPs, etc.
1. Local Infrastructure (docker-compose)
We’ll create a docker-compose.yml for our three base services.
version: "3.8"
services:
fdb:
image: foundationdb/foundationdb:7.4.0
container_name: fdb
command: >
fdbserver
--listen_address 0.0.0.0:4500
--public_address fdb:4500
--cluster_file /var/fdb/fdb.cluster
--datadir /var/fdb/data
--logdir /var/fdb/logs
volumes:
- fdb-data:/var/fdb/data
- ./fdb.cluster:/var/fdb/fdb.cluster:ro
ports:
- "4500:4500"
restart: unless-stopped
minio:
image: minio/minio
container_name: minio
environment:
MINIO_ROOT_USER: minio
MINIO_ROOT_PASSWORD: minio12345
command: server /data --console-address ":9001"
ports:
- "9000:9000"
- "9001:9001"
volumes:
- minio-data:/data
restart: unless-stopped
app:
build:
context: .
dockerfile: docker/Dockerfile
container_name: meta-app
depends_on:
- fdb
- minio
environment:
MINIO_ENDPOINT: http://minio:9000
MINIO_ACCESS_KEY: minio
MINIO_SECRET_KEY: minio12345
FDB_CLUSTER_FILE: /etc/foundationdb/fdb.cluster
volumes:
- ./fdb.cluster:/etc/foundationdb/fdb.cluster:ro
ports:
- "8080:8080"
restart: unless-stopped
volumes:
fdb-data:
minio-data:
To obtain the cluster file for the Java client:
docker cp fdb:/var/fdb/fdb.cluster ./fdb.cluster
2. Key Schema in FDB (Tuple Layer)
Root: ("b")
("b", tenant, objId, "meta") -> JSON { bucket, object, size, sha256, contentType, etag, createdAt, tags: {k:v} }
("b", tenant, objId, "state") -> BYTE (0=pending, 1=committed)
For the MVP, we’ll only write meta and state=1.
Later, you can add prefix indexes, tags, or owner-based lookups.
Conventions
tenant: short string (e.g."acme")objId: logical identifier within tenant (UUIDv7 recommended)
3. Minimal API (Javalin)
PUT /o/{tenant}/{id}
Registers or updates metadata for an existing MinIO object.
Body JSON:
{
"bucket": "metas",
"object": "imgs/networking.png",
"contentType": "image/png",
"size": 12345,
"sha256": "...",
"tags": { "owner": "you or me", "kind": "img" }
}
Validates object existence in MinIO (via statObject)
→ stores meta + state=1 in FDB (transaction).
Idempotent per (tenant, id).
GET /o/{tenant}/{id}
Returns JSON metadata.
HEAD /o/{tenant}/{id}
Returns 200 if exists, 404 if not.
GET /o/{tenant}?prefix=abc
Lists object IDs starting with prefix abc (range scan).
MVP: fixed limit (e.g. 100).
DELETE /o/{tenant}/{id}?deleteBlob=true|false
Deletes metadata; if deleteBlob=true, also attempts MinIO deletion.
4. Java Project (Gradle)
plugins {
id 'java'
id 'application'
id 'com.github.johnrengelman.shadow' version '8.1.1'
}
group = 'org.fmartinez'
version = '1.0-SNAPSHOT'
repositories { mavenCentral() }
java { toolchain { languageVersion = JavaLanguageVersion.of(21) } } // 21 LTS
dependencies {
implementation "org.foundationdb:fdb-java:7.4.3"
implementation "io.minio:minio:8.6.0"
implementation "io.javalin:javalin:6.7.0"
implementation "com.fasterxml.jackson.core:jackson-databind:2.20.0"
runtimeOnly "org.slf4j:slf4j-simple:2.0.16"
testImplementation platform('org.junit:junit-bom:5.10.0')
testImplementation 'org.junit.jupiter:junit-jupiter'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
application {
mainClass = 'Server'
}
tasks.test { useJUnitPlatform() }
Repository: https://github.com/fmartinez09/metadata
5. Containerization (Docker)
Why?
The Java binding requires the native FoundationDB client installed on the host (or in PATH/LD_LIBRARY_PATH).
On Windows, it tries to load fdb_java.dll (JNI) from the JAR and then fdb_c.dll from the system.
As of 7.4.x, the published Maven JAR does not include fdb_java.dll for Windows, so we must run it in Linux (Docker image with JDK).
We’ll create a multi-stage Dockerfile using JDK 24 + foundationdb-clients 7.4, connecting to FDB and MinIO over Docker network (avoiding DLL headaches).
# --- Build stage: compile and create fat-jar ---
FROM gradle:8.9-jdk21 AS build
WORKDIR /app
COPY . ./
RUN gradle shadowJar -x test
# --- FDB stage: extract libfdb_c.so ---
FROM foundationdb/foundationdb:7.4.0 AS fdb
# --- Runtime stage: JRE + libfdb_c.so copied ---
FROM eclipse-temurin:21-jre
COPY --from=fdb /usr/lib/libfdb_c.so /usr/lib/libfdb_c.so
ENV MINIO_ENDPOINT=http://minio:9000 \
MINIO_ACCESS_KEY=minio \
MINIO_SECRET_KEY=minio12345 \
FDB_CLUSTER_FILE=/etc/foundationdb/fdb.cluster
COPY --from=build /app/build/libs/*-all.jar /app/app.jar
COPY docker/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh && mkdir -p /etc/foundationdb
EXPOSE 8080
ENTRYPOINT ["/entrypoint.sh"]
entrypoint.sh
This script automates startup tasks. It ensures that the Java app has access to the fdb.cluster file.
Steps:
- Check if
/etc/foundationdb/fdb.clusterexists. - If not, create it with the line
docker:docker@fdb:4500. - Print its contents.
- Launch the app (
java -jar /app/app.jar).
#!/usr/bin/env bash
set -euo pipefail
if [ ! -f "${FDB_CLUSTER_FILE}" ]; then
echo "docker:docker@fdb:4500" > "${FDB_CLUSTER_FILE}"
fi
echo "Using FDB_CLUSTER_FILE at ${FDB_CLUSTER_FILE}:"
cat "${FDB_CLUSTER_FILE}" || true
echo "Starting app..."
exec java -jar /app/app.jar
⚠️ Notes:
- Requires the
FDB_CLUSTER_FILEvariable (because ofset -u). - Doesn’t overwrite an existing file.
- The JAR must be self-contained (
ShadowJaror Spring Boot fat-jar).
Directory Layout
metadata/ # Gradle project root
├── build.gradle
├── settings.gradle
├── src/
│ └── main/java/... # Server.java, FdbStore.java, etc.
├── fdb.cluster # shared cluster file (1 line: docker:docker@fdb:4500)
├── docker/
│ ├── Dockerfile
│ ├── entrypoint.sh
├── docker-compose.yml
6. Build the Image (generates fat-jar in build stage)
Create fdb.cluster:
PowerShell:
"docker:docker@fdb:4500" | Out-File -NoNewline -Encoding ascii .\fdb.cluster
Bash:
echo docker:docker@fdb:4500 > fdb.cluster
Build:
docker compose build
Run:
docker compose up -d
Check app logs (confirm cluster file loaded):
docker compose logs -f app
7. Initialize the FDB Database (first time only)
FoundationDB starts but the cluster is initially “unconfigured”.
docker compose exec fdb fdbcli --cluster_file /var/fdb/fdb.cluster --exec "configure new single ssd"
docker compose exec fdb fdbcli --cluster_file /var/fdb/fdb.cluster --exec "status minimal"
Once you see Database available, you’re ready.
8. Create the MinIO Bucket (once)
The service validates object existence in MinIO, so we need a bucket.
You can create it via the web console (http://localhost:9001, user: minio, pass: minio12345)
or using the mc client:
mc alias set local http://localhost:9000 minio minio12345
mc mb local/metadata
mc ls local
9. Register Metadata in FDB
Access FDB:
docker compose exec fdb fdbcli --cluster_file /var/fdb/fdb.cluster
To inspect key ranges:
getrangekeys "" \xff
PUT
$body = @{
bucket="metadata";
object="networking.jpg";
size=(Get-Item .\networking.jpg).Length;
sha256="DF47E064EFC7A9ABC31E3E556BC0E43CF933738D4C843D9F41139194F28638EC";
contentType="image/jpeg";
tags=@{ owner="fernando"; tipo="foto" }
} | ConvertTo-Json -Depth 3
Invoke-RestMethod -Method PUT -Uri "http://localhost:8080/o/tenant1/foto-001" -ContentType "application/json" -Body $body
GET (read metadata)
Invoke-RestMethod -Method GET -Uri "http://localhost:8080/o/tenant1/foto-001"
HEAD (check existence)
$resp = Invoke-WebRequest -Method HEAD -Uri "http://localhost:8080/o/tenant1/foto-001"; $resp.StatusCode # 200 or 404
LIST (IDs per tenant)
Invoke-RestMethod -Method GET -Uri "http://localhost:8080/o/tenant1"
DELETE (metadata only)
Invoke-RestMethod -Method DELETE -Uri "http://localhost:8080/o/tenant1/foto-001" -SkipHttpErrorCheck
DELETE (metadata + blob in MinIO)
Invoke-RestMethod -Method DELETE -Uri "http://localhost:8080/o/tenant1/foto-001?deleteBlob=true" -SkipHttpErrorCheck
✅ Done. You’re working with the most powerful database on the market.
To clean everything up:
docker compose down -v
