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 objectThis 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?.name3. 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 MappingBenefits
1. Single Source of Truth
// Only one place data lives: ObjectPool
// Object Graph is views over that data
// Can't get out of sync2. 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 neededKey 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 WireRowNext Steps
- Learn about Lazy Collections - How collections work in the graph
- Learn about Lazy References - How references work
- Learn about Identity Mapping - Same ID = same instance
- Learn about Synchronous API - How it all comes together
- See Architecture - System design