Chapter 08 · Redis JSON and Document-Oriented Data
Document Modeling: Embedding, Duplication, Key Design, Size, and Partial Updates
Model bounded Redis JSON documents with explicit embedding, duplication, schema-version, key-lifecycle, memory, and migration tradeoffs.
Learning outcomes
AtlasMart can now store and mutate nested documents, but document shape is an architectural decision. Embedding everything into one product or customer document can create giant hot keys; splitting every property into a Redis key can create cardinality and lifecycle problems. This lesson builds a deliberate middle ground.
Choose embedding versus references/duplication from update and read patterns.
Design stable Redis keys and explicit schema-version fields for JSON documents.
Bound arrays and nested objects so documents do not grow invisibly forever.
Use partial updates while preserving source-of-truth and duplication rules.
Measure document memory/payload characteristics instead of declaring JSON or Hash universally better.
All Chapter 08 mandatory labs reuse the disposable Chapter 01
environment: Redis Open Source 8.10.1 from Docker
Official Image redis:8.10.1, container
atlasmart-redis-ch01, standalone topology, host
publication 127.0.0.1:6379, TLS disabled only
because traffic stays on loopback, default ACL user disabled,
named ACL users atlasmart-app and
academy-admin, logical database 0, AOF with
appendfsync everysec plus RDB snapshots,
persistent /data volume, and no explicit Redis
maxmemory limit or eviction policy. Redis 8
integrates JSON and Search capabilities in Redis Open Source,
so the pinned image is the mandatory free/local path; no
separate Redis Stack image or managed service is required. The
primary client is the redis-cli shipped in the
same image. Fixtures are bounded under
atlasmart:ch08:*.
1. Model around access and change boundaries
Embedding is useful when sub-data is read with the parent, updated under the same lifecycle, and has bounded size. Separate keys are useful when sub-data has independent lifecycle, very different access frequency, separate authorization, or potentially unbounded cardinality.
| AtlasMart data | Likely shape | Why |
|---|---|---|
| product dimensions | embed in product JSON | small, bounded, usually read with product |
| recent product badges | bounded embedded array | small display-oriented collection with explicit cap |
| all product reviews | separate review keys/stream/index | unbounded growth and independent lifecycle |
| inventory per warehouse | depends on update/hotness | can embed when bounded; separate/shard when high-frequency or independently authorized |
| customer payment secrets | do not casually embed | security and access boundary is stronger than convenience |
2. Stable key naming stays outside the JSON schema
The Redis key locates the document operationally; fields inside the document describe business data. Keep identity derivable and avoid putting volatile attributes into the key.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app DEL atlasmart:ch08:product:2001docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.SET atlasmart:ch08:product:2001 '$' '{"schemaVersion":1,"productId":2001,"sku":"PACK-2001","name":"City Pack","badges":["new"],"supplier":{"id":88,"displayName":"Northwind"}}'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.GET atlasmart:ch08:product:2001 '$.productId' '$.sku'
The duplicated product ID inside the document can help validation/debugging, but the application must define which identity is authoritative if they ever disagree.
3. Duplication is a consistency contract
AtlasMart may duplicate supplier display name into a product read model to avoid another lookup. That is not inherently wrong, but it creates a synchronization rule: when the supplier name changes, old product snapshots can become stale.
| Duplication choice | Benefit | Cost / repair mechanism |
|---|---|---|
| no duplication | single authoritative value | extra lookup or composition |
| duplicate display field | fast local read | event/update fan-out plus reconciliation |
| duplicate immutable ID only | stable reference | still needs lookup for current display fields |
Document databases do not erase distributed consistency; they move it into modeling choices.
4. Schema version is explicit application data
Redis JSON accepts valid JSON, not “AtlasMart Product v2.” Put an explicit version in the document and make readers/writers define supported versions.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app DEL atlasmart:ch08:schema:v1 atlasmart:ch08:schema:v2docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.SET atlasmart:ch08:schema:v1 '$' '{"schemaVersion":1,"name":"Pack","priceCents":5000}'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.SET atlasmart:ch08:schema:v2 '$' '{"schemaVersion":2,"name":"Pack","pricing":{"amountCents":5000,"currency":"AZN"}}'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.GET atlasmart:ch08:schema:v1 '$.schemaVersion' '$.priceCents'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.GET atlasmart:ch08:schema:v2 '$.schemaVersion' '$.pricing.amountCents'
A v2 Search schema or client class does not transform v1 automatically. Migration is a data operation with rollback and compatibility strategy.
5. Additive evolution is usually easier than destructive renaming
A staged migration can first teach readers both shapes, then write the new shape, backfill old documents, switch readers, and only later remove the old field. This makes rollback possible during the compatibility window.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.SET atlasmart:ch08:schema:v1 '$.pricing' '{"amountCents":5000,"currency":"AZN"}'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.SET atlasmart:ch08:schema:v1 '$.schemaVersion' '2'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.GET atlasmart:ch08:schema:v1 '$'
In a real migration, validate that the old amount and new amount agree before deleting the old path, and record counts/errors rather than assuming every document is conformant.
6. Bound arrays explicitly
Embedded arrays are attractive because they serialize naturally, but an unbounded reviews/history array turns one key into a large mutable object. Keep only the bounded subset that belongs in the parent view.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.ARRAPPEND atlasmart:ch08:product:2001 '$.badges' '"featured"' '"weekend"' '"clearance"'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.ARRLEN atlasmart:ch08:product:2001 '$.badges'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.ARRTRIM atlasmart:ch08:product:2001 '$.badges' -3 -1docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.GET atlasmart:ch08:product:2001 '$.badges'
The trim is a policy choice, not a magic production number. Pick caps from product semantics, payload/memory data, and update frequency.
7. Giant-document failure mode
A practitioner may embed every customer order, event, review, and click inside one customer JSON because nested arrays are convenient. The result is a hot, ever-growing key whose reads, persistence copy-on-write impact, replication traffic, backup size, and mutation latency can all worsen.
Split unbounded histories into their own structures—Streams, Sorted Sets, review documents, or indexed records—while the customer JSON keeps only bounded summary/current-state fields. Verify the relationship with IDs and reconciliation.
8. Partial updates reduce payload, not logical coupling
A narrow write such as updating
$.supplier.displayName avoids root replacement, but
the whole document is still one Redis key for ACL key-pattern
purposes, key expiration, persistence, replication, eviction,
and Cluster routing.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.SET atlasmart:ch08:product:2001 '$.supplier.displayName' '"Northwind Trading"'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.GET atlasmart:ch08:product:2001 '$.supplier'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app TTL atlasmart:ch08:product:2001docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app MEMORY USAGE atlasmart:ch08:product:2001
9. TTL is key-level lifecycle
Redis key expiration applies to the entire JSON key. Do not assume nested members independently expire because Hash field-expiration exists elsewhere in Redis. If one embedded fragment needs a radically shorter lifecycle, that is a modeling signal to consider a separate key or explicit application cleanup.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app EXPIRE atlasmart:ch08:product:2001 300docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app TTL atlasmart:ch08:product:2001docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.SET atlasmart:ch08:product:2001 '$.name' '"City Pack 2"'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app TTL atlasmart:ch08:product:2001
A path mutation does not create a field TTL. Validate exact key-TTL preservation rules in your target release/client whenever lifecycle behavior is business-critical.
10. Security boundaries are not JSON paths
The Chapter 01 ACL user is scoped to
atlasmart:* keys and command categories. That does
not mean Redis ACLs automatically authorize “this caller can see
$.public but not $.private” inside one
JSON document. If sub-document fields have different
authorization requirements, application-layer filtering or
separate keys/services may be safer.
11. Compare memory with equivalent bounded representations
Build the same tiny record as JSON and Hash and observe, without turning one sample into a universal rule.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app DEL atlasmart:ch08:compare:json atlasmart:ch08:compare:hashdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.SET atlasmart:ch08:compare:json '$' '{"sku":"A-1","name":"Adapter","stock":7}'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app HSET atlasmart:ch08:compare:hash sku A-1 name Adapter stock 7docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app MEMORY USAGE atlasmart:ch08:compare:jsondocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app MEMORY USAGE atlasmart:ch08:compare:hash
Repeat with realistic field counts/value sizes and enough keys before drawing a capacity conclusion. Redis internal encodings and allocator behavior are version/data dependent.
12. Migration must preserve observability and rollback
For an online schema migration, record at least: source document count, version distribution, conversion successes/failures, old/new field agreement, Search index coverage if relevant, memory delta, latency distribution, and rollback window. A migration script that “ran without exception” is not sufficient evidence.
13. Reproducible modeling lab
Create one intentionally bounded product, mutate a duplicated read-model field, and prove schema/version/memory state.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app DEL atlasmart:ch08:lesson3docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.SET atlasmart:ch08:lesson3 '$' '{"schemaVersion":2,"productId":3001,"name":"Bottle","supplier":{"id":9,"displayName":"Aqua"},"recentLabels":["new","eco"]}'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.SET atlasmart:ch08:lesson3 '$.supplier.displayName' '"Aqua Co"'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.ARRAPPEND atlasmart:ch08:lesson3 '$.recentLabels' '"featured"'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.ARRTRIM atlasmart:ch08:lesson3 '$.recentLabels' -3 -1docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.GET atlasmart:ch08:lesson3 '$.schemaVersion' '$.supplier' '$.recentLabels'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app MEMORY USAGE atlasmart:ch08:lesson3docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app DEL atlasmart:ch08:lesson3
14. Production judgment
Choose document boundaries from lifecycle, authorization, cardinality, update hotness, read composition, and failure recovery. Embedded JSON is excellent for bounded cohesive records, but it can become a hot/big key when used as an unlimited aggregate. Keep schema versions explicit, migrations observable, duplicates reconcilable, and TTL/security boundaries aligned with key boundaries.
15. Summary and next step
You can now model JSON documents without defaulting to “embed everything” or “split everything.” Next, we add a deliberately scoped Redis Search index and keep its schema/migration state distinct from stored JSON.
Check your understanding
- When is embedding usually attractive?
- Why is duplicated supplier displayName a consistency contract?
- Does a JSON path have its own Redis key TTL?
- Why is one small MEMORY USAGE comparison insufficient to choose Hash versus JSON?
- What should an online schema migration measure?
Review the answers
When sub-data is bounded, shares lifecycle/authorization, and is commonly read with the parent.
The duplicate can become stale and needs an update/reconciliation rule.
No. Redis expiration applies to the containing key.
Internal encodings, value sizes, field counts, allocator behavior, and workload shape can reverse the result.
Version distribution, conversion success/failure, old/new agreement, index coverage when relevant, memory/latency changes, and rollback state.
Authoritative references
- Redis JSON data type — JSON storage, typed operations, and command overview
- JSONPath syntax — modern dollar-path selection, wildcards, filters, slices, and recursive descent
- JSON.SET — root/path writes plus NX/XX and path-creation rules
- JSON.GET — serialized return shapes for modern and legacy paths
- JSON.TYPE — type introspection and modern/legacy return behavior
- JSON.OBJKEYS — object-key introspection and complexity
- JSON.ARRLEN — array length and multi-match behavior
- JSON.NUMINCRBY — atomic numeric path mutation
- JSON.STRAPPEND — atomic string append and resulting lengths
- JSON.ARRAPPEND — array append and resulting lengths
- JSON.MERGE — RFC 7396 merge-patch behavior for objects and arrays
- Index JSON documents — FT.CREATE ON JSON, JSONPath schema attributes, and indexing behavior
- FT.CREATE — Search index schema, prefix scope, and field types
- FT.INFO — index metadata and observable indexing state
- MEMORY USAGE — per-key memory evidence and sampling
- Redis 8.10 release notes — pinned server release family
- JSON.ARRTRIM — bounded array trimming and complexity
- Redis key expiration — key-level TTL behavior