53 lines
2.1 KiB
Markdown
53 lines
2.1 KiB
Markdown
|
|
# ADR-002: Identity as a Value Type
|
||
|
|
|
||
|
|
**Status:** Accepted
|
||
|
|
**Date:** 2026-03-18
|
||
|
|
|
||
|
|
## Context
|
||
|
|
|
||
|
|
`Identity` travels through the request context from authentication middleware to
|
||
|
|
handler and downstream service calls. When a type travels via `context.WithValue`,
|
||
|
|
callers retrieve it with a type assertion. If the stored type were a pointer
|
||
|
|
(`*Identity`), any middleware could mutate the struct fields after the value was
|
||
|
|
placed in context, causing non-obvious bugs in concurrent or pipelined middleware
|
||
|
|
chains. Additionally, pointer types retrieved from context require nil checks at
|
||
|
|
every retrieval site.
|
||
|
|
|
||
|
|
## Decision
|
||
|
|
|
||
|
|
`Identity` is declared as a plain struct, not a pointer:
|
||
|
|
|
||
|
|
```go
|
||
|
|
type Identity struct {
|
||
|
|
UID string
|
||
|
|
TenantID string
|
||
|
|
DisplayName string
|
||
|
|
Email string
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
`NewIdentity` returns `Identity` (value), not `*Identity`. `SetInContext` stores the
|
||
|
|
value directly. `FromContext` retrieves it as a value and returns `(Identity, bool)`;
|
||
|
|
the boolean indicates absence without requiring a nil pointer check.
|
||
|
|
|
||
|
|
Enrichment methods return new values rather than mutating the receiver.
|
||
|
|
`WithTenant(id string) Identity` copies the receiver, sets `TenantID` on the copy,
|
||
|
|
and returns the copy. The original is unchanged, making it safe to call from
|
||
|
|
concurrent middleware.
|
||
|
|
|
||
|
|
The two-step construction pattern is intentional:
|
||
|
|
1. `NewIdentity(uid, displayName, email)` — populated from the auth token.
|
||
|
|
2. `id.WithTenant(tenantID)` — optionally enriched by a tenant-resolution middleware.
|
||
|
|
|
||
|
|
## Consequences
|
||
|
|
|
||
|
|
- No nil checks are needed at retrieval sites. A zero-value `Identity{}` is returned
|
||
|
|
when no identity is in context; the `bool` return of `FromContext` distinguishes
|
||
|
|
"not authenticated" from "authenticated with empty fields".
|
||
|
|
- Concurrent middleware cannot accidentally mutate an `Identity` already stored in
|
||
|
|
context — each enrichment step produces a new value.
|
||
|
|
- Copying `Identity` on every `WithTenant` call is negligible: four string fields,
|
||
|
|
each a pointer-and-length pair internally.
|
||
|
|
- The compliance test enforces that `WithTenant` returns `Identity` (not `*Identity`)
|
||
|
|
at compile time, preventing a future regression to a pointer receiver.
|