JSON Persistence Explained

How PersonCarDog saves the in-memory Repository to a JSON file
and why this is the perfect bridge to Spring Boot + JPA

Core idea: The Repository acts as an in-memory database. JsonStore is the “disk” layer that serializes the whole database into a single JSON file (data/repo.json) and restores it later. This is exactly the role a real database (H2, PostgreSQL…) plays in Spring Boot.

1. Architecture Mapping (Java SE → Spring Boot)

Controller
Controller.java
→ @RestController
Service
Service.save / load
→ @Service + @Transactional
Repository
Repository (ArrayLists)
→ Spring Data JPA Repo
Persistence
JsonStore + Snapshot
→ Database + EntityManager
Current project (Java SE)

JsonStore

  • File-based persistence
  • One JSON file = whole DB
  • Manual mapping (DTO ↔ Entity)
  • No transactions, no SQL
  • Perfect for teaching
Spring Boot equivalent

JPA / Hibernate

  • Real database (H2, Postgres…)
  • Tables + foreign keys
  • Automatic mapping (@Entity)
  • @Transactional, ACID
  • Production-ready

2. Why do we need a Snapshot (DTO)?

Domain objects have object references (Person → Car, Person ↔ Dog). If we serialize them directly with Jackson we get:

Solution: convert the object graph into a flat, ID-based structure (exactly like foreign keys in a relational database).

Object Graph (in memory)

Person Alice
car → Car#1
dog → Dog#2
Car#1
Toyota Corolla
Dog#2
Buddy (owner → Alice)
DogAdoption
adopter → Alice
dog → Dog#2

Cycles & shared references → cannot be serialized directly

toSnapshot()
→
IDs instead of objects

RepositorySnapshot (JSON-ready)

{ "people": [ { "id": "p1", "name": "Alice", "carId": "c1", "dogId": "d2" } ], "cars": [ { "id": "c1", "make": "Toyota", "model": "Corolla", "year": 2020 } ], "dogs": [ { "id": "d2", "name": "Buddy", "ownerId": "p1" } ], "dogAdoptions": [ { "id": "a1", "adopterId": "p1", "dogId": "d2", "fee": 50 } ] }

3. SAVE Flow (Repository → JSON file)

ServicesaveRepository()
→
JsonStore.save
→
toSnapshot()entities → DTOs
→
JacksonwriteValue()
→
data/repo.json
  1. Service calls JsonStore.save(repo, path)
  2. toSnapshot() walks every list in the Repository and builds flat DTOs, replacing object references with id strings (foreign-key style).
  3. Jackson’s ObjectMapper writes the Snapshot as pretty-printed JSON.
  4. Parent directories are created automatically if needed.

4. LOAD Flow (JSON file → Repository) — Two-Pass Strategy

data/repo.json
→
JacksonreadValue()
→
RepositorySnapshot
→
fromSnapshot()2 passes
→
Repositoryready

Pass 1 — Create entities

  1. Instantiate every Car, Person, Dog with the persisted ID
  2. Put them into temporary Maps keyed by ID
  3. Add them to the Repository

At this point references are still null / missing.

Pass 2 — Wire references

  1. Look up each entity by ID from the Maps
  2. Set person.car, person.dog, dog.owner
  3. Rebuild CarTransaction & DogAdoption objects with real object references

Now every relation is a single shared instance (no duplicates).

Why two passes? You cannot set a reference to an object that does not exist yet. First create everything, then connect the dots — the same strategy ORMs use when loading bidirectional associations.

5. Key Classes

Class Role Spring Boot analogue
JsonStore Save / load the whole Repository EntityManager + Database
RepositorySnapshot Flat, cycle-free DTO container Database schema (tables + FKs)
*Dto inner classes Simple POJOs with id + foreign-key ids @Entity columns + @JoinColumn
ObjectMapper (Jackson) JSON serialization / deserialization Hibernate dialect + JDBC
Service.saveRepository / loadRepository Public API + error handling @Service method with Result / ResponseEntity

6. Default File Location

Default path defined in JsonStore:

data/repo.json

Relative to the project working directory. The parent folder is created automatically on save.

7. Teaching Takeaways for Spring Boot

Bottom line for students:
You are already implementing the core persistence concepts (object graph → flat storage → object graph) with pure Java + Jackson. When you move to Spring Boot you will keep the same domain model and simply let JPA/Hibernate handle the Snapshot and the two-pass loading for you.