Java / Partial Data
Designing for Partial Data: Distinguishing “We Don’t Know” From “False”
Sealed types and exhaustive pattern matching keep “unknown” distinct from “zero” throughout a service.
Partial data is normal. Missing signals and unverifiable sources happen. If you collapse “unknown” into “false” or “zero,” you erode trust. As an engineering manager, I’ve learned to design services that treat “we don’t know” as a first-class outcome. In Java, sealed types and exhaustive pattern matching make this straightforward and enforceable.
The failure mode
Your aggregator treats null as “0.0,” sums four dimensions, and displays a neat number. A caller misreads that as “complete and low.” In reality, two dimensions were missing, and one was unverifiable. The number masked uncertainty.
What I learned
Keep partial states in the domain layer, not just at the edges
Don’t convert unknowns to defaults at the boundary. Let domain logic operate on sealed types. If you must produce a single number, force policy conversion in a dedicated function so that the decision is auditable.
Policy example
public final class AggregationPolicy {
public static double conservativeAverage(List<ScoreOrUnknown> dims) {
int count = 0;
double sum = 0.0;
for (ScoreOrUnknown d : dims) {
switch (d) {
case Score sc -> { sum += sc.value(); count++; }
case Missing miss -> { /* exclude; track missing elsewhere */ }
case Unverifiable unv -> { /* exclude; track unverifiable */ }
}
}
if (count == 0) return Double.NaN; // no numeric evidence
return sum / count;
}
public static boolean okToDisplay(List<ScoreOrUnknown> dims) {
long unknowns = dims.stream().filter(x -> !(x instanceof Score)).count();
return unknowns == 0; // or choose a threshold
}
}
Propagate uncertainty deliberately
Unknowns should travel with the data: into logs, decisions, and UI. For example, if two dimensions are unknown, surface that count alongside any numeric aggregation. The goal is to prevent silent conversions that distort confidence.
Make unknown-handling policies explicit and testable
Write tests that assert what happens when all dimensions are unknown, when some are missing, and when a single unverifiable dimension appears. Pattern matching keeps these tests aligned with the domain states. Don’t assume a “best effort” numeric default; decide and encode.
Resist “default values” in DTOs
If you serialize to JSON, include a type field in each dimension and omit value when the state isn’t Score. A consumer reading {"type":"Missing","reason":"timeout"} can act accordingly. Defaulting to 0.0 or “unknown=false” invites misuse.
Teach callers to branch by type, not numeric checks Documentation should show switch examples, not “if value is 0 then unknown.” This reduces coupling and preserves room for future states (e.g., “Contested,” “Deprecated”).
Audit conversion points with a linelist List every place where ScoreOrUnknown becomes a single number or a boolean. Audit these conversions periodically. If you discover accidental flattening, refactor to preserve states or add proper policy functions.
Accept that aggregation may return “no number” Returning NaN or omitting a field is reasonable when inputs are fully unknown. Treat this as normal, not exceptional. UI can show “insufficient evidence” with counts; APIs can document this outcome clearly.
JSON serialization with a type field
// Using a simple custom serializer sketch
public record Score(double value) implements ScoreOrUnknown {}
public record Missing(String reason) implements ScoreOrUnknown {}
public record Unverifiable(String note) implements ScoreOrUnknown {}
public Map<String, Object> toJson(ScoreOrUnknown s) {
return switch (s) {
case Score sc -> Map.of("type", "Score", "value", sc.value());
case Missing miss -> Map.of("type", "Missing", "reason", miss.reason());
case Unverifiable unv -> Map.of("type", "Unverifiable", "note", unv.note());
};
}
Trade-offs and failure modes
Trade-offs
Transparency vs brevity: Returning types and reasons increases payload size but improves clarity. Favor clarity when ambiguity matters.
Strictness vs convenience: Throwing on invalid scores and disallowing defaults catches bugs early but may frustrate callers used to flexible shapes.
NaN vs explicit omission: NaN signals “no numeric value” but can be mishandled by consumers. Omitting a field or providing a separate state field avoids confusion.
Failure modes
“Silent flattening”
A downstream transforms unknowns to zero—likely out of habit. Catch this in contract tests.
“Enum drift”
If reason codes evolve, consumers break. Document codes and provide a safe default handling path.
“Partial measures hiding in averages”: An average of one numeric with three unknowns looks “complete” unless you also surface unknown counts.
Try it yourself
Refactor one DTO to embed a sealed state rather than a nullable numeric field. Add a type field in JSON. Update consumers to switch on type.
Write aggregation policies that return NaN or omit fields when inputs are fully unknown. Test behavior explicitly.
Add metrics that count unknowns and unverifiable states separately from numeric distributions.