ADR-0052: IAM Service Consolidation
Date: 2025-11-23 Status: Accepted Decision Makers: Platform Team
Context
During the development of Citadel's IAM stack, we split what should have been a single authorization service into 5 separate microservices:
- iam-service - User-tenant mappings, roles
- policy-service - ABAC attributes and policies
- customization-service - Runtime schema definitions for attributes
- tenant-lifecycle-service - Tenant state transitions
- permissions-service - ReBAC wrapper around SpiceDB
The Problems This Created
Cache Invalidation Hell:
- Token enrichment required querying multiple services
- Claims had to be assembled from iam-service + policy-service + customization-service
- Cache invalidation across services became a distributed coordination problem
Distributed Monolith:
- Each service was too small to be useful independently
- Together they formed tight coupling through API dependencies
- Deployment complexity increased without architectural benefit
Forever Rewrite Syndrome:
- Every week the boundaries were reconsidered
- Development velocity dropped to near-zero
- 5 projects were delivered using Keycloak + SpiceDB + Temporal while this was being "perfected"
What Worked: Book-Keeper
In contrast, book-keeper succeeded because:
- One responsibility: double-entry ledger
- Clear boundaries: doesn't do invoicing, payments, or business logic
- Shipped and done: 92% test coverage, stable, forever useful
- Composable: any product can use it via simple API
Decision
Consolidate IAM responsibilities into a single iam-service:
iam-service Owns:
- Users (sub/identifier only, no PII)
- Tenants (with status: active/suspended/archived)
- User-tenant mappings
- Roles (global and tenant-scoped)
- ABAC attributes (user and tenant attributes)
- Token claims enrichment
- Future: Schema registry for custom attributes (use JSONB for now)
What Stays Separate (These Are NOT Overengineered):
- user-directory-service - Solves real problem: routing PII queries to 100+ IdPs
- client-directory-service - Solves real problem: managing OAuth clients across IdPs
- permissions-service - Keep only if ReBAC graph queries are actually needed
What Gets Archived:
- policy-service - Merge into iam-service
- customization-service - Premature optimization, use JSONB metadata fields
- tenant-lifecycle-service - Just a status field, not a service
Consequences
Positive:
- Simpler token enrichment: Single database join instead of microservice orchestration
- Faster development: One codebase, one deployment, one database
- No cache invalidation hell: Claims data is colocated
- Actually ship: Focus on building products, not perfecting platform internals
Negative:
- Slightly larger service: But still focused on single domain (authorization policy)
- Migration effort: Need to consolidate existing policy-service work into iam-service
Neutral:
- Still composable: Other services consume iam-service via token claims or API
- Still multi-tenant: Tenant scoping remains enforced
Implementation
Phase 1: Documentation (This ADR)
- Archive overengineered service checklists
- Update ROADMAP.md to reflect consolidation
- Update iam-service checklist with Phase 8 responsibilities
- Move ADRs to _archived with notices
Phase 2: Code Consolidation (Future Sprint)
- Add
statusfield to Tenant aggregate in iam-service - Add
user_attributesandtenant_attributestables to iam-service - Implement attribute CRUD APIs in iam-service
- Update token enrichment to include attributes
- Add
metadata: jsonbfields to users/tenants for flexible custom data
Phase 3: Cleanup (Future Sprint)
- Remove policy-service repository (if it exists)
- Remove customization-service repository
- Update deployment manifests
Lessons Learned
The Book-Keeper Principle
"One responsibility, ship it, done, forever useful."
When designing platform services:
- Does it solve ONE cross-cutting concern?
- Can it be used independently?
- Does it provide a reusable primitive?
- Is it too small to be useful alone?
- Does it create distributed state management?
Avoid Premature Separation
Don't split services until:
- You have multiple products using the pattern
- The service is becoming a bottleneck
- There's clear independent value
ERR on the side of monolith, split only when painful.
Multi-IdP Router is NOT Overengineering
user-directory-service and client-directory-service solve REAL problems:
- Supporting 100+ IdPs without coupling to iam-service
- Providing unified API for PII queries
- Adapter pattern for different IdP implementations
References
- Archived ADRs:
policy-service(ADR-0029 through ADR-0033) - Archived ADRs:
customization-service(ADR-0026 through ADR-0028) - See
website/docs/04-services/_archived/for full archived service documentation iam-service checklist:notes/checklists/platform/p0-foundation/10-iam-service.mdROADMAP.md