IAM Service: Component Breakdown
This document provides a deep dive into the internal architecture of the iam-service, following the principles of Domain-Driven Design (DDD) and Clean Architecture as established in ADR 0001: Standardized Service Layout. It breaks down the components and their interactions for key use cases, serving as a blueprint for developers.
The architecture is layered to separate concerns:
- API Layer: The entrypoint for external requests (e.g., REST controllers).
- Application Layer: Orchestrates use cases by coordinating domain objects and infrastructure.
- Domain Layer: Contains the core business logic, entities, and rules.
- Infrastructure Layer: Implements external concerns like databases, message brokers, and third-party APIs.
Visualizing the Flow
The following sequence diagram illustrates how a request flows through the layers for the "Create Tenant" use case. Note that we use a standard Layered Architecture with Application Services, rather than a strict CQRS Command Bus.
Use Case: Create a New Tenant
This is the primary workflow, triggered by POST /tenants.
1. api Layer (Entrypoint)
- DTO (
CreateTenantDTO):- File:
internal/api/v1/dtos/tenant.go - Purpose: Defines the shape of the incoming JSON request body with validation tags.
- File:
- Handler (
TenantHandler):- File:
internal/api/v1/handlers/tenants.go - Purpose: Defines the HTTP handler for
POST /tenants. It parses the DTO and calls the application service.
- File:
2. application Layer (Orchestration)
- Service (
TenantService):- File:
internal/application/services/tenant_service.go - Purpose: The core orchestrator for the use case. It uses domain objects and infrastructure interfaces to execute the business workflow.
- File:
3. domain Layer (Business Logic & Rules)
- Bounded Context:
identity - Aggregate (
Tenant):- File:
internal/domain/identity/aggregates/tenant.go - Purpose: The core
Tenantentity. It holds the state (ID, Name, Status) and enforces business rules (invariants).
- File:
- Value Objects (
TenantID,TenantName):- File:
internal/domain/identity/value_objects/tenant_name.go, etc. - Purpose: Ensures type safety and encapsulates validation for simple values.
- File:
- Factory (
TenantFactory):- File:
internal/domain/identity/aggregates/tenant.go(Factory methods often reside with the aggregate) - Purpose: Encapsulates the logic for creating a new, valid
Tenantaggregate.
- File:
- Repository Interface (
TenantRepository):- File:
internal/domain/identity/repositories/tenant_repository.go - Purpose: An interface that defines the contract for how
Tenantaggregates are persisted (e.g.,Save,FindByID).
- File:
- Domain Event (
TenantCreated):- File:
internal/domain/identity/events/tenant_events.go - Purpose: A data structure representing the fact that a new tenant was successfully created.
- File:
4. infrastructure Layer (External Details)
- Repository Implementation (
GormTenantRepository):- File:
internal/infrastructure/persistence/postgres/tenant_repository.go - Purpose: The concrete implementation of the
TenantRepositoryinterface using a specific technology like GORM/PostgreSQL.
- File:
- Event Bus (
NatsEventBus/KafkaEventBus):- File:
internal/infrastructure/eventbus/nats_adapter.go - Purpose: Implements the
ports.EventBusinterface. It publishes domain events to the configured message broker (NATS or Kafka).
- File:
- Transactional Outbox:
- Mechanism: Instead of publishing directly to the bus during the HTTP request, events are often saved to an
outboxtable in the same transaction as the aggregate. A background poller then picks them up and publishes them via theEventBus.
- Mechanism: Instead of publishing directly to the bus during the HTTP request, events are often saved to an
5. Wiring It All Together
- Container Builder: A container builder function (e.g., in
internal/app/container.go) uses manual wiring to construct all the concrete infrastructure components and inject them into the application services. - CLI Commands (
app/cmd/): The application is structured around Cobra CLI commands. Theservecommand (app/cmd/serve.go) initializes the container and starts the API server.
Use Case: Get a Tenant by ID (Query)
This workflow is triggered by GET /tenants/{tenant_id}.
1. api Layer
- Handler (
TenantHandler):- File:
internal/api/v1/handlers/tenants.go - Purpose: Defines the HTTP handler for
GET /tenants/{tenant_id}. It parses the ID and calls the service.
- File:
2. application Layer
- Service (
TenantService):- File:
internal/application/services/tenant_service.go - Purpose: The orchestrator for the read operation. It uses the repository to fetch the data.
- File:
3. infrastructure Layer
- Repository (
GormTenantRepository):- File:
internal/infrastructure/persistence/postgres/tenant_repository.go - Purpose: A concrete implementation that queries the database.
- File:
Testing Strategy
We will use an "outside-in" TDD approach, where tests are written from the perspective of the user/client and drive the implementation of internal components.
-
API Layer Test (Behavior/Integration Test):
- Tool:
net/http/httptest - Goal: Write a test that makes a real HTTP request to the
POST /tenantsendpoint and asserts the expected outcome (e.g., HTTP 202 Accepted, command bus was called). This test drives the creation of the router and DTO.
- Tool:
-
Application Layer Test (Logic/Integration Test):
- Tool:
testify/mock - Goal: Write a test for the
CreateTenantCommandHandler. All external dependencies (repositories, adapters) are mocked. This test verifies the orchestration logic: that the correct domain methods and infrastructure interfaces are called in the right order.
- Tool:
-
Domain Layer Test (Unit Test):
- Tool: Standard
testingpackage. - Goal: Write pure unit tests for the
Tenantaggregate, its factory, and value objects. These tests are fast, have zero dependencies, and verify the core business rules and invariants of the system.
- Tool: Standard
Example Test Flow for "Create Tenant"
-
Write the API Test:
func TestCreateTenant_Endpoint_Success(t *testing.T)- It fails because the
/tenantsroute doesn't exist. - Implement: Create the
TenantRouterand theCreateTenanthandler method to make the test pass.
-
Write the Application Handler Test:
func TestCreateTenant_Handler_Success(t *testing.T)- It fails because the handler logic is empty.
- Implement: Write the orchestration logic inside
CreateTenantCommandHandler.Handle()using mocked dependencies.
-
Write the Domain Unit Tests:
func TestTenantFactory_Create_Success(t *testing.T)func TestTenantFactory_Create_EmptyName_Fails(t *testing.T)- They fail because the factory logic is missing.
- Implement: Write the business logic in the
TenantFactoryandTenantaggregate to satisfy the tests.
Use Case: Claims Enrichment
This is the primary runtime workflow of the service, triggered by the API Gateway calling POST /v1/system/enrich-token.
1. api Layer
- Handler (
SystemHandler):- File:
internal/api/v1/handlers/system.go - Purpose: Defines the handler for
/system/enrich-token. It extracts the token and tenant hint, calls the service, and sets the response headers from the result.
- File:
2. application Layer
- Service (
ClaimsService):- File:
internal/application/services/claims_service.go - Purpose: Orchestrates the enrichment by calling the
ClaimsProviderto validate the token and theUserRepositoryto fetch local policy.
- File:
3. infrastructure Layer
- Claims Provider (
JWTClaimsProvider,IntrospectionClaimsProvider): Implements the logic to get claims from the upstream IdP by either validating a JWT or calling an introspection endpoint. - Claims Provider Factory (
ClaimsProviderFactory): Selects the correctClaimsProviderbased on the IdP configuration. - Repository Implementation (
GormUserRepository): Implements theFindByIdpUserIDAndTenantIDmethod to query the database for the user's specific policy record.
Use Case: List My Tenants
This workflow allows an authenticated user to retrieve a list of all tenants they are a member of. It is triggered by GET /me/tenants.
1. api Layer
- Handler (
UserHandler):- File:
internal/api/v1/handlers/users.go - Purpose: Defines the handler for
GET /me/tenants. It extracts the authenticated user's ID from the context and calls the service.
- File:
2. application Layer
- Service (
UserService):- File:
internal/application/services/user_service.go - Purpose: Orchestrates the query by calling the user repository.
- File:
3. infrastructure Layer
- Repository Implementation (
GormUserRepository):- File:
internal/infrastructure/persistence/postgres/user_repository.go - Purpose: Implements the
FindTenantsByIdpUserIDmethod, which queries the database to find all tenants associated with a user.
- File:
Use Case: Create a New User
This workflow is triggered by POST /tenants/{tenant_id}/users.
1. api Layer
- DTO (
CreateUserDTO):- File:
internal/api/v1/dtos/user.go - Purpose: Defines the request body for creating a user (e.g.,
name,email,password).
- File:
- Handler (
UserHandler):- File:
internal/api/v1/handlers/users.go - Purpose: Defines the handler for
POST /tenants/{tenant_id}/users. It parses the DTO andtenant_idfrom the path, then calls the service.
- File:
2. application Layer
- Service (
UserService):- File:
internal/application/services/user_service.go - Purpose: Orchestrates user creation. It will:
- Fetch the
Tenantaggregate to ensure it exists. - Use a
UserFactoryto create a newUseraggregate.
- Fetch the
- File:
3. domain Layer
- Aggregate (
User):- File:
internal/domain/identity/aggregates/user.go - Purpose: A new aggregate root representing a user. It holds state like
ID,Name,Email,Status, and a list ofRoles.
- File:
- Repository Interface (
UserRepository):- File:
internal/domain/identity/repositories/user_repository.go - Purpose: Defines the contract for persisting
Useraggregates.
- File:
- Domain Event (
UserCreated):- File:
internal/domain/identity/events/user_events.go
- File:
4. infrastructure Layer
- Repository Implementation (
GormUserRepository):- File:
internal/infrastructure/persistence/postgres/user_repository.go - Purpose: The concrete implementation of the
UserRepositoryinterface using GORM.
- File:
Use Case: Assign Attributes to a User
This workflow is triggered by POST /tenants/{tenant_id}/users/{user_id}/attributes. It replaces the traditional role assignment with a more flexible Attribute-Based Access Control (ABAC) model.
1. api Layer
- DTO (
AssignAttributesDTO):- File:
internal/api/v1/dtos/attribute.go - Purpose: Defines the request body:
{"key": "role", "value": "admin"}.
- File:
2. application Layer
- Service (
UserService):- File:
internal/application/services/user_service.go - Purpose: Orchestrates attribute assignment. It will:
- Fetch the
Useraggregate. - Validate the attribute using
YAMLSchemaValidator(if applicable). - Call
user.SetAttribute(...). - Save the updated
Useraggregate.
- Fetch the
- File:
3. domain Layer
- Aggregate (
User):- The
Useraggregate has aSetAttributemethod.
- The
- Validator (
YAMLSchemaValidator):- File:
internal/domain/policy/schema_validator.go - Purpose: Ensures that attributes assigned to users conform to defined schemas (e.g., allowed values for "role").
- File:
- Domain Event (
UserAttributeAssigned):- File:
internal/domain/identity/events/user_events.go
- File:
4. infrastructure Layer
- Repository Implementation (
GormUserRepository):- The existing repository's
Savemethod will handle persisting the updatedUseraggregate.
- The existing repository's