ADR-111: Flatten All DynamoDB Records to Top-Level Attributes¶
Status: Accepted Date: 2026-01-25 Issue: #180
Context¶
The codebase accumulated three DynamoDB schema patterns: nested data.M maps (entities, limits, audit events, version records), hybrid (buckets with one flat counter), and flat (usage snapshots and config records). ADR-006 and ADR-010 introduced flat attributes to solve DynamoDB's "overlapping document paths" limitation. ADR-101 standardized flat schema for all new config records and explicitly recommended flattening existing record types in v0.6.0.
The mixed patterns create inconsistency in serialization/deserialization code, complicate the aggregator Lambda (which must navigate nested paths), and make the codebase harder to maintain. Every new feature must decide which pattern to use, adding cognitive overhead.
Decision¶
All DynamoDB record types must use flat schema (top-level attributes, no nested data.M wrapper). Deserialization reads flat format only. Pre-1.0.0 semver allows breaking changes without migration — existing nested records are not supported. Serialization produces only flat format.
Consequences¶
Positive:
- Uniform schema across all record types eliminates pattern inconsistency
- Simpler serialization code without data.M wrapper construction
- Aggregator Lambda reads flat attributes directly instead of navigating nested paths
- Enables atomic operations on any attribute without "overlapping paths" errors
- No branching logic in deserializers
Negative:
- Existing nested data.M records from pre-0.6.0 deployments must be recreated (no migration path provided pre-1.0.0)
- DynamoDB reserved words (name, resource, action, timestamp) require ExpressionAttributeNames aliases in all expressions
Alternatives Considered¶
Keep nested data.M for existing records¶
Rejected because: Perpetuates schema inconsistency; ADR-101 already committed to flattening in v0.6.0.
Big-bang migration (write stops until all records migrated)¶
Rejected because: Requires downtime; pre-1.0.0 semver makes migration unnecessary — breaking changes are expected.
Flatten only on read (no serialization changes)¶
Rejected because: Leaves old-format records indefinitely; new writes would still produce nested format, preventing convergence.