Public API
The public types and methods of dedup4j. · 5 min
The types dedup4j expects you to use. Everything here is in
com.edem.dedup4j.*.
Records, not beans
Every model type is an immutable record with validation in its compact
constructor. There are no setters, and an invalid instance cannot be
constructed.
Entry points
BlobStore
The upload facade. Auto-configured by the starter.
public interface BlobStore {
BlobReference store(MultipartFile file);
BlobReference store(Path path);
BlobReference store(byte[] content, String filename, String contentType);
BlobReference store(InputStream content, long sizeBytes, String filename,
String contentType, Map<String, String> metadata);
BatchStoreResult storeAll(MultipartFile[] files);
}
See Uploading & deduplication.
BlobDeduplicationService
Retrieval and reference counting. Auto-configured.
public interface BlobDeduplicationService {
BlobReference store(StoreBlobCommand command);
void retain(UUID assetContentId);
void release(UUID assetContentId);
BlobResource get(UUID assetContentId);
BlobLocation location(UUID assetContentId);
}
Its store takes a StoreBlobCommand directly — the lower-level entry point
BlobStore delegates to. See
Retrieval, retain & release.
Models
BlobReference
Returned by every store.
public record BlobReference(
UUID assetContentId,
ContentHash contentHash,
String contentType,
String storageProvider,
String bucketOrContainer,
String objectKey,
boolean duplicate
) {
BlobLocation location();
}
assetContentId is the handle to persist. duplicate reports whether the
bytes already existed.
ContentHash
public record ContentHash(String algorithm, String hash, long sizeBytes) {}
Content identity is the whole triple. The algorithm is SHA-256.
BlobResource
Returned by get. Holds an open stream and implements AutoCloseable.
public record BlobResource(
String objectKey,
InputStream content,
long sizeBytes,
String contentType,
Map<String, String> metadata
) implements AutoCloseable {}
try (BlobResource resource = blobs.get(id)) { ... }
BlobLocation
public record BlobLocation(
String provider,
String bucketOrContainer,
String objectKey
) {}
Note the component is provider, while BlobReference calls the same concept
storageProvider.
Everything needed to build a presigned URL with your provider's own SDK — see why dedup4j does not issue them.
StoreBlobCommand
public record StoreBlobCommand(
InputStream content,
String filename,
String contentType,
long sizeBytes,
Map<String, String> metadata
) {}
Batch results
public sealed interface BatchStoreOutcome
permits BlobStoreSuccess, BlobStoreFailure {
int index();
String filename();
}
public record BlobStoreSuccess(int index, String filename, BlobReference reference)
implements BatchStoreOutcome {}
public record BlobStoreFailure(int index, String filename, RuntimeException failure)
implements BatchStoreOutcome {}
public record BatchStoreResult(List<BatchStoreOutcome> outcomes) {
List<BlobStoreSuccess> successes();
List<BlobStoreFailure> failures();
boolean allSucceeded();
}
Sealed, so a switch over outcomes is exhaustively checked. index() maps
back to the position in the array you submitted.
Extension points
Each is an auto-configured bean declared @ConditionalOnMissingBean — declare
your own and the default steps aside.
BlobStorage
The storage SPI. Implement it to support a provider dedup4j does not ship.
public interface BlobStorage {
StoredBlob put(PutBlobRequest request);
BlobResource get(String objectKey);
void delete(String objectKey);
boolean exists(String objectKey);
}
delete must be idempotent — an already-missing object counts as deleted.
public record PutBlobRequest(
String objectKey, InputStream content, long sizeBytes,
String contentType, String originalFilename, Map<String, String> metadata
) {}
public record StoredBlob(
String objectKey, String provider, String bucketOrContainer,
long sizeBytes, String contentType, String checksum, Instant createdAt
) {}
See custom providers.
ObjectKeyStrategy
Decides where content lands in the store.
public interface ObjectKeyStrategy {
String generateKey(ContentHash contentHash);
}
The key is derived from content identity alone — the same bytes always produce the same key, which is what makes deduplication work. A strategy that introduces randomness breaks that.
ContentHasher
public interface ContentHasher {
ContentHash hash(InputStream inputStream) throws IOException;
}
Changing this orphans existing content
Identity is the hash. Existing rows keep hashes from the old algorithm, so previously stored content will never match again and will be stored a second time.
Reconciliation
Not auto-configured — construct it yourself.
public interface LogicalReferenceCountSource {
Map<UUID, Long> countLogicalReferences();
}
public record ReconciliationMismatch(
UUID assetContentId,
long expectedReferenceCount,
long actualReferenceCount
) {}
public record ReconciliationReport(
long checkedContentCount,
List<ReconciliationMismatch> mismatches
) {}
LogicalReferenceCountSource is a functional interface: you tell dedup4j what
the counts should be, because it cannot enumerate your records.
Upload history
UploadHistory (com.edem.dedup4j.history) is an auto-configured bean,
declared @ConditionalOnMissingBean. It reads the persistent record of
completed uploads and failures. See
Observability & dashboards.
public interface UploadHistory {
HistoryTotals totals();
List<FailureRecord> recentFailures(Instant since, int limit); // newest first, for display
List<FailureRecord> failuresAfter(long afterSequenceNo, int limit); // ascending, for paging
boolean persistent();
}
public record HistoryTotals(
String scope, String datasetId,
long uploads, long duplicates, long physicalWrites,
long logicalBytes, long avoidedBytes
) {
long newUploads(); // uploads - duplicates
long skippedPhysicalWrites(); // uploads - physicalWrites
}
public record FailureRecord(
long sequenceNo, UUID id, Instant recordedAt, String operation,
String message, String instanceId, String contentHash, String objectKey
) {}
scopeis"all-time"(persistent ledger) or"since-start"(history disabled; process-local counters).datasetIdisnullwhen the totals are not tied to a dataset.operationisuploadordelete.- Page failures for ingestion with
failuresAfter, using the lastsequenceNoyou saw as the cursor.recentFailuresis for display only. persistent()isfalsewhen history is disabled.
The interface also declares recordUpload and recordFailure, which the
library calls itself. Do not call them from application code: an upload
recorded outside the library's transaction is a total that never happened.
FailureSource
Dedup4jManagementSnapshot.FailureSource (management module) feeds the
management failures endpoint. Implement it to supply your own failures.
@FunctionalInterface
public interface FailureSource {
List<Failure> recentFailures(Instant since);
default List<Failure> failuresAfter(long afterSequence, int limit) {
return List.of();
}
}
failuresAfter defaults to an empty list, meaning the source does not support
sequence paging; ?afterSequence= then returns nothing and only
recentFailures serves its failures. Such a source also gets none of its
failures into the standalone dashboard: the dashboard ingests by an
afterSequence cursor, and falls back to since only when the items it
receives lack a sequenceNo. An empty page is not that case.
Exceptions
All unchecked. See Troubleshooting.
Dedup4jException
├── BlobValidationException
├── BlobStorageException
├── BlobHashingException
├── ContentNotFoundException
└── ReferenceCountUnderflowException
DuplicateContentIdentityException ← extends RuntimeException directly