Skip to main content

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:

  1. iam-service - User-tenant mappings, roles
  2. policy-service - ABAC attributes and policies
  3. customization-service - Runtime schema definitions for attributes
  4. tenant-lifecycle-service - Tenant state transitions
  5. 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 status field to Tenant aggregate in iam-service
  • Add user_attributes and tenant_attributes tables to iam-service
  • Implement attribute CRUD APIs in iam-service
  • Update token enrichment to include attributes
  • Add metadata: jsonb fields 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:

  1. Does it solve ONE cross-cutting concern?
  2. Can it be used independently?
  3. Does it provide a reusable primitive?
  4. Is it too small to be useful alone?
  5. 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.md
  • ROADMAP.md