Hydrator
Source: data/app/lib/hydrator/hydrator.go
Meet the Hydrator — a lightweight mechanism for declaring which related entities (associations) should be loaded alongside a primary entity. Rather than loading every association by default or passing ad-hoc boolean flags, callers register named hydrating paths on a HydratorMod and fetchers check those paths before performing the associated data-fetch work. This keeps queries minimal and makes data-loading behaviour explicit at the call site.
Table of Contents
Overview
The hydrator package provides a structured alternative to boolean flags or string lists for controlling association loading. Callers register named hydrating paths on a HydratorMod before passing it to a fetcher or presenter layer. Each layer then calls Contains to decide whether to execute the SQL for a particular association.
Hydrators are typically used in fetcher and presenter layers to avoid N+1 query patterns — only the associations explicitly requested via HydratorMod are loaded.
Data Structures
The hydrator package defines three types. Together they form the complete vocabulary for expressing and checking association-load requests.
// Hydrator
type Hydrator struct{}
A stateless service struct with no fields. Exists solely as a receiver for utility methods, keeping the API consistent with the rest of the lib packages that use dependency injection.
// HydratorMod
type HydratorMod struct {
Paths []*HydratingPath
}
The DTO that accumulates the set of paths a caller wants hydrated. Passed down through fetchers and presenters so each layer can check which associations to load.
| Field | Type | Description |
|---|---|---|
| Paths | []*HydratingPath |
Ordered list of hydrating paths registered by the caller |
// HydratingPath
type HydratingPath struct {
Path []string
}
Represents a single association path as an ordered slice of string segments. A single segment (["Comments"]) describes a direct association; multiple segments could describe a nested path (["Comments", "Author"]).
| Field | Type | Description |
|---|---|---|
| Path | []string |
Ordered path segments identifying an association |
Methods
HydratorMod
The HydratorMod type exposes two methods — one for registering paths and one for checking whether a path was registered.
// AddHydratingPath
func (this *HydratorMod) AddHydratingPath(items ...string)
Appends a new HydratingPath constructed from the variadic items to mod.Paths. Callers use this to declare which associations they want loaded before passing the mod to a fetcher.
// Example
mod.AddHydratingPath("Comments")
mod.AddHydratingPath("Author", "Profile")
// Contains
func (this *HydratorMod) Contains(items ...string) bool
Returns true if any registered path exactly matches the given items (same length, same elements in the same order). Fetchers call this to decide whether to execute the SQL for a particular association.
// Example
if mod.Contains("Comments") {
// load comments
}
Hydrator
The Hydrator struct provides one method — a nil-safe guard that normalizes incoming mods before fetchers begin their work.
// ModDefaulting
func (this *Hydrator) ModDefaulting(mod *HydratorMod) *HydratorMod
Nil-safe guard: returns mod unchanged if it is non-nil, otherwise returns a new empty &HydratorMod{}. Fetchers call this at their entry point so the rest of the method can call mod.Contains(...) without a nil check.
💡 Tip: Always call
ModDefaultingas the first line of any fetcher method that accepts a*HydratorMod. This lets callers passnilwhen they want no associations loaded, without requiring nil-checks scattered throughout the fetcher’s logic.
Architectural Patterns
The Hydrator package applies four patterns that keep association-loading predictable and safe.
Explicit opt-in loading: Rather than loading all associations by default, callers declare exactly which paths they need. This keeps queries minimal and makes the data-loading behaviour visible at the call site.
Path equality by value:
Containsusesslices.Equalfor exact, ordered comparison. A path["Comments"]does not match["Author", "Comments"], preventing accidental over-hydration from partial matches.Nil-safe defaults:
ModDefaultinglets callers passnilwhen they want no associations loaded. The fetcher layer normalises it to an empty mod without special-casingnilthroughout its logic.Stateless service:
struct Hydratorhas no fields. It is injected as a dependency so callers get a consistent API surface and the struct can be swapped or extended without changing call sites.
Usage Example
The following example shows the full lifecycle: a caller registers paths, a fetcher normalises the mod, then each association is conditionally loaded.
// Caller (e.g. a handler or presenter) declares which associations to load.
mod := &hydrator.HydratorMod{}
mod.AddHydratingPath("Comments")
mod.AddHydratingPath("Tags")
// Inside a fetcher, guard against nil mods first.
mod = h.Hydrator.ModDefaulting(mod)
// Conditionally load each association.
if mod.Contains("Comments") {
thing.Comments = fetchComments(ctx, thing.Id)
}
if mod.Contains("Tags") {
thing.Tags = fetchTags(ctx, thing.Id)
}