Skip to Content
LearnCore ConceptsObject Graph

Object Graph

Concept: Connected model instances that form a graph structure and automatically stay in sync.

What is an Object Graph?

An Object Graph is a network of connected model objects where:

  • Objects reference each other (teams → issues → users)
  • Same ID always returns same instance (Identity Mapping)
  • Changes to one object automatically update all references
  • MobX makes it reactive (changes trigger re-renders)

Structure

Think of it like a tree, but it’s actually a graph with cross-references:

Workspace (root) ├── teams: Collection<Team> │ ├── Team { id: '1', name: 'Engineering' } │ │ ├── issues: LazyCollection<Issue> │ │ ├── labels: Collection<Label> │ │ └── members: Collection<User> │ └── Team { id: '2', name: 'Design' } │ └── ... ├── users: Collection<User> │ ├── User { id: 'alice', name: 'Alice' } │ │ ├── assignedIssues: LazyCollection<Issue> │ │ └── notifications: LazyCollection<Notification> │ └── User { id: 'bob', name: 'Bob' } │ └── ... └── projects: Collection<Project> └── ... Issue (connected to multiple parts of the graph) ├── team: Reference<Team> // Points back to Team ├── assignee: LazyReference<User> // Points to User ├── comments: LazyCollection<Comment> └── labels: Collection<Label>

How It’s Built

1. ObjectPool (Flat Storage)

Data starts as a flat map of WireRows:

ObjectPool = Map { 'team:1' → { t: 'team', id: '1', p: { name: 'Engineering' } } 'issue:10' → { t: 'issue', id: '10', p: { title: 'Bug', teamId: '1', assigneeId: 'alice' } } 'user:alice' → { t: 'user', id: 'alice', p: { name: 'Alice' } } }

No relationships yet - just raw data with foreign keys.

2. Graph Construction

IdentityMap builds the Object Graph by “hoisting” relationships:

// Raw data has FK: issue.p.teamId = '1' issue.p.assigneeId = 'alice' // IdentityMap creates references: issue.team → Team { id: '1' } issue.assignee → LazyReference<User> // And inverse collections: team.issues → LazyCollection<Issue> (includes issue with id: '10') user.assignedIssues → LazyCollection<Issue>

3. Identity Mapping

IdentityMap ensures same ID = same instance:

const issue1 = useModel('issue', '10') const issue2 = useModel('issue', '10') console.log(issue1 === issue2) // true ✓ // Exact same JavaScript object

This is what makes the Object Graph work - all references point to the same underlying instances.

Relationship Types

Gluonic supports 5 relationship types:

1. One-to-Many (Collection)

Parent has collection of children:

@ClientModel('team') class Team extends Model { @OneToMany('team') issues: LazyCollection<Issue> } // Usage team.issues.map(issue => issue.title)

2. Many-to-One (Reference)

Child references parent:

@ClientModel('issue') class Issue extends Model { @ManyToOne('issues', 'teamId') team: Reference<Team> @ManyToOne('assignedIssues', 'assigneeId') assignee: LazyReference<User> } // Usage const teamName = issue.team.name const assigneeName = issue.assignee.value?.name

3. Many-to-Many (Collections both ways)

Multiple items on both sides:

@ClientModel('issue') class Issue extends Model { @ManyToMany('issues') labels: Collection<Label> } @ClientModel('label') class Label extends Model { @ManyToMany('labels') issues: LazyCollection<Issue> } // Usage - bidirectional issue.labels.map(label => label.name) label.issues.map(issue => issue.title)

4. One-to-One

Single reference both ways:

@ClientModel('user') class User extends Model { @OneToOne('user') profile: LazyReference<Profile> } @ClientModel('profile') class Profile extends Model { @OneToOne('profile') user: Reference<User> }

5. Unidirectional Reference

Reference without inverse:

@ClientModel('favorite') class Favorite extends Model { // Points to issue, but issue doesn't point back @Reference() issue: LazyReference<Issue> }

Graph Stays in Sync Automatically

When data changes anywhere, the entire graph updates:

// Update an issue await store.save('issue', '10', { title: 'New Title' }) // All references update automatically (same instance): issue.title // 'New Title' ✓ team.issues.find(i => i.id === '10').title // 'New Title' ✓ user.assignedIssues.find(i => i.id === '10').title // 'New Title' ✓ // Single source of truth!

Why Object Graph vs Database Queries?

Traditional Approach (Database Queries)

const TeamView = ({ teamId }) => { const [team, setTeam] = useState(null) const [issues, setIssues] = useState([]) useEffect(() => { // Separate queries fetchTeam(teamId).then(setTeam) fetchIssues(teamId).then(setIssues) }, [teamId]) // Problem: Data can get out of sync // Problem: Multiple sources of truth // Problem: Manual updates required }

Gluonic Approach (Object Graph)

const TeamView = observer(({ teamId }) => { const team = useModel<Team>('team', teamId) // Just navigate the graph return ( <div> <h1>{team.name}</h1> {team.issues.map(issue => ( <IssueRow key={issue.id} issue={issue} /> ))} </div> ) }) // Single source of truth: ObjectPool // Automatic updates: MobX // Always consistent: Identity Mapping

Benefits

1. Single Source of Truth

// Only one place data lives: ObjectPool // Object Graph is views over that data // Can't get out of sync

2. Automatic Consistency

// Update anywhere... await store.save('user', 'alice', { name: 'Alice Smith' }) // ...visible everywhere instantly team.members.find(u => u.id === 'alice').name // 'Alice Smith' ✓ issue.assignee.value?.name // 'Alice Smith' ✓ user.name // 'Alice Smith' ✓

3. Memory Efficient

// Despite appearing in many places, only one copy in memory team1.members[0] === team2.members[0] // true (same user) issue.assignee.value === user // true (same instance)

4. Natural Navigation

// Navigate the graph naturally const authorOfFirstComment = issue.comments.at(0)?.author.value const teamOfAssignee = issue.assignee.value?.teams.at(0) // Reads like plain JavaScript // No special query language needed

Key Insight: The Object Graph is what makes the synchronous API possible. By keeping data in memory as connected objects, you can navigate relationships instantly without async queries.

Object Graph vs IdentityMap

Don’t confuse these terms:

  • Object Graph: The connected model instances in memory (the data structure you use)
  • IdentityMap: The identity map that builds and maintains the Object Graph (implementation detail)
  • ObjectPool: The flat storage that holds raw data (the Object Graph is built from this)
// Object Graph = What you navigate team.issues.map(...) issue.assignee.value // IdentityMap = How it's built (behind the scenes) const bridge = new IdentityMap(store) bridge.getModel('team', '1') // Returns same instance every time // ObjectPool = Where raw data lives (you rarely access directly) store.pool.get('team', '1') // Raw WireRow

Next Steps

Last updated on