Identity Service
Accounts, sessions and a multi-level partner hierarchy for a multi-tenant platform
Background
Every other service on the platform depends on identity: who the user is, which tenant they belong to, and where they sit in a partner hierarchy that can be ten levels deep. It has to answer "all descendants of X" quickly, keep sessions revocable, and stay consistent when account creation spans several services.
Architecture
Gateway → identity service (go-zero gRPC) → MySQL via a db/ layer of composable GORM scopes, with Redis behind a cache/ layer and a cacheSync/ layer that orchestrates read-through and asynchronous write-back. Account changes are published to Kafka with before/after snapshots and trace headers, so the CRM read model and segmentation services can follow them.
Key Highlights
Closure-table hierarchy (ancestor, descendant, depth) for a 10-level partner network, written inside the account-creation transaction
Three-tier data access: MySQL → Redis → asynchronous cache sync with fanout workers, singleflight and TTL jitter
Redis Bloom filter for account de-duplication; JWT sessions with sliding refresh and server-side revocation
Extracted into a standalone gRPC microservice (92 RPCs at the split); added about 20 more RPCs afterwards, including closure-table hierarchy queries
Compensating rollback for multi-service account creation; the registration event is published only after every step succeeds
Cache-layer hardening: bounded async worker pools with queue-full metrics, a synchronous fallback for session revocation, and batched cache-miss loading
Key Implementations
Closure-table partner hierarchy
Stored every ancestor–descendant pair with its depth, written in the same transaction that creates the account, and built subtree, downline and per-tenant directory queries on top of it.
Why: Subtree queries ("everyone under this partner") are the most common read. A closure table answers them with one indexed lookup instead of recursive queries, and depth makes the 10-level limit easy to enforce.
Three-tier cache with asynchronous sync
Reads go through singleflight to the database on a miss. Cache write-back and invalidation run on a bounded async worker pool with TTL jitter. In 2025 I hardened it: corrected miss handling, confined singleflight to the DB read, added per-call timeouts and queue-full counters, and made session revocation fall back to synchronous when the pool is saturated.
Why: The identity service sits on every request path. Collapsing concurrent misses, spreading expiry and never dropping a revocation matter more here than anywhere else.
Result: Per-user cache-miss lookups were replaced by one batched query and a pipelined write.
Consistent multi-service account creation
Creating an account also creates wallets and settings in other services. Each flow was split into validate, prepare and setup steps, with a compensating delete on an independent timeout-safe context if a later step fails.
Why: Without it, a downstream failure left half-created users behind, and downstream consumers could see a registration event for an account that no longer existed.
Technical Decisions
| Technical Decisions | Chosen | Alternative | Reason |
|---|---|---|---|
| Hierarchy storage | Closure table | Adjacency list with recursive queries | Reads vastly outnumber hierarchy changes, and nearly every read is a subtree query. Paying extra rows on insert buys single-lookup subtree reads. |
| Session revocation | One active token per user in Redis alongside the JWT | Pure stateless JWT | Admins must be able to disable an account and force a logout immediately; a stateless token can't be revoked before it expires. |