hrsystem: oncecache example
May 13, 2024 · View on GitHub
hrsystem contains a oncecache example. It is a simple staff directory backed by an
in-memory "database". It uses oncecache to reduce DB calls.
The Raison d'être of hrsystem is to demonstrate cache entry propagation across a
composite cache.
Consider a trivial HR system:
---
title: HR System
---
erDiagram
Org ||--|{ Department : contains
Org {
string name
}
Department ||--|{ Employee : contains
Department {
string name
}
Employee {
int id
string name
string role
}
In Go, we might model this system as:
type Org struct {
Name string `json:"name"`
Departments []*Department `json:"departments"`
}
type Department struct {
Name string `json:"name"`
Staff []*Employee `json:"staff"`
}
type Employee struct {
Name string `json:"name"`
Role string `json:"role"`
ID int `json:"id"`
}
type HRSystem interface {
GetOrg(ctx context.Context, org string) (*Org, error)
GetDepartment(ctx context.Context, dept string) (*Department, error)
GetEmployee(ctx context.Context, ID int) (*Employee, error)
}
Now, consider when HRSystem.GetOrg is called: it returns the
entire constructed tree containing all Departments, which in turn contain
all Employees. We could use a oncecache.Cache[string, *Org] to cache
the Org objects.
But, later, we might want to retrieve a single Employee
via HRSystem.GetEmployee(ctx, 1234). Typically, the HRSystem impl would fetch that Employee
from the database, but note that the Employee is already present in the Org cache, as
a child of a Department object.
We can use oncecache to propagate cache entries across composite caches.
// NewHRCache wraps db with a caching layer.
func NewHRCache(log *slog.Logger, db HRSystem) *HRCache {
c := &HRCache{
log: log,
db: db,
}
c.orgs = oncecache.New[string, *Org](
db.GetOrg,
oncecache.OnFill(c.onFillOrg),
)
c.depts = oncecache.New[string, *Department](
db.GetDepartment,
oncecache.OnFill(c.onFillDept),
)
c.employees = oncecache.New[int, *Employee](
db.GetEmployee,
)
return c
}
In the code above, the NewHRCache constructor adds OnFill event handlers to the
Org and Department caches. Thus, a Get call to the Org cache will trigger
onFillOrg:
// onFillOrg is invoked by HRCache.orgs when that cache fills an [Org] value from
// the DB. This handler propagates values from the returned [Org] to the
// HRCache.depts cache.
func (c *HRCache) onFillOrg(ctx context.Context, _ string, org *Org, err error) {
if err != nil {
return
}
for _, dept := range org.Departments {
// Filling a dept entry should in turn propagate to the employees cache.
_ = c.depts.MaybeSet(ctx, dept.Name, dept, nil)
}
}
When onFillOrg is invoked, it iterates over the Departments in the Org and
calls MaybeSet on the Department cache. This in turn triggers the onFillDept,
which invokes MaybeSet on the Employee cache:
// onFillDept is invoked by HRCache.depts when that cache fills a [Department]
// value from the DB. This handler propagates [Employee] values from the
// returned [Department] to the HRCache.employees cache.
func (c *HRCache) onFillDept(ctx context.Context, _ string, dept *Department, err error) {
if err != nil {
return
}
for _, emp := range dept.Staff {
_ = c.employees.MaybeSet(ctx, emp.ID, emp, nil)
}
}