Case Study

Architecture Decision Records

20 ADRs documenting every significant architectural decision — context, alternatives considered, and rationale for each trade-off.

Why ADRs Matter

Every significant architectural decision in this project is documented as an Architecture Decision Record. ADRs capture why a decision was made, not just what was built. They show how I think about trade-offs: what alternatives were considered, what constraints existed, and what the consequences are.

These aren't retroactive documentation — they were written at the time each decision was made, forming a decision log that any team member could reference.

All Architecture Decision Records

001
Architecture Showcase Scope
Three architectural paths side-by-side: monolith, microservices, and strangler-fig migration.
Accepted

Context

This repository demonstrates real-world engineering and architectural decision-making. A portfolio that only demonstrates one style hides important tradeoffs: operability vs simplicity, consistency vs autonomy, migration strategy vs greenfield purity.

Decision

Intentionally include three architectural approaches within the same domain:

  • Clean Architecture Monolith — maintainability, DDD boundaries, CQRS, operational readiness
  • Microservices — service decomposition and distributed workflows
  • Connector API (Strangler-Fig) — incremental migration and integration patterns

Consequences

  • Demonstrates breadth: design, migration, integration, distributed concerns
  • Enables "compare and contrast" discussions in interviews
  • Increased repo complexity and cognitive load (mitigated by ADRs and clear boundaries)
002
Dapr Usage Boundaries
Dapr for local dev portability, Azure managed services for production.
Accepted

Context

Dapr provides pub/sub, configuration, and secrets for local development. The target production environment is Azure, where first-party managed services provide stronger guarantees.

Decision

Use Dapr as an integration abstraction for local development, with a clear exit strategy for Azure:

CapabilityLocalAzure Target
ConfigurationDapr + RedisAzure App Configuration
SecretsDapr + VaultAzure Key Vault
Pub/SubDapr + RabbitMQAzure Service Bus

Consequences

  • Strong local developer experience without Azure dependency
  • Clear migration path to managed services
  • Demonstrates real-world cloud transition thinking
003
Transactional Outbox
Atomic state changes + event publishing with retries and dead-letter handling.
Accepted

Context

Publishing integration events directly after saving data can lead to inconsistencies: DB commits without event publish, or vice versa.

Decision

Implement a transactional outbox: persist an outbox record in the same transaction as the state change. A background processor publishes undispatched records with retry policies.

Consequences

  • Strong reliability guarantees — no "lost events"
  • Controlled retries and backoff strategies
  • Slight delay between commit and delivery (eventual consistency)
  • UPDLOCK, READPAST locking for concurrency safety
004
Observability-First
Systems judged by diagnosis speed, release safety, and operational confidence.
Accepted

Context

For senior roles, operability is a core competency. Portfolios often omit this.

Decision

Treat observability as a first-class design constraint:

  • Distributed tracing across services and key internal operations
  • Structured logs with correlation identifiers
  • Health checks separated into liveness/readiness and dependency checks
  • Trace/span names reflect business operations, not framework internals

Consequences

  • Faster debugging and root-cause analysis
  • Stronger "production-readiness" signal for reviewers
  • Adds up-front work and ongoing discipline
005
CQRS + Decorator Pipeline
Custom decorator chain instead of MediatR with explicit cross-cutting concerns.
Accepted

Context

Many projects implement CQRS with MediatR. We want to demonstrate CQRS as a pattern, not a library dependency.

Decision

Implement CQRS with explicit Command/Query contracts, handlers, and a decorator pipeline:

  • Validation — FluentValidation rules
  • Transaction — DB transaction (skip with INoTransaction marker)
  • Observability — logging, metrics, OpenTelemetry spans
  • UserContext — auth check + user sync

Consequences

  • Reduced framework lock-in; clearer intent
  • Cross-cutting concerns composable and testable in isolation
  • Slightly more scaffolding than MediatR
006
RabbitMQ vs Azure Service Bus
Environment-specific messaging: RabbitMQ for dev, Service Bus for prod.
Accepted

Context

The portfolio demonstrates event-driven communication. Messaging infrastructure should be intentionally selected based on environment.

Decision

RabbitMQ locally for fast iteration and visibility. Azure Service Bus for production with enterprise-grade durability.

CapabilityRabbitMQAzure Service Bus
HostingSelf-managedFully managed
Local DevExcellentPoor (cloud-only)
Dead-LetteringYesYes (first-class)
Ops OverheadHigherLower

Consequences

  • Clean separation between domain logic and transport
  • Realistic dev-to-prod story
  • Requires contract stability and idempotent consumers
007
Trace Context Propagation
W3C TraceContext across HTTP, messaging, and background processors.
Accepted

Context

Debugging distributed systems requires correlating frontend requests with backend execution.

Decision

  • Propagate W3C Trace Context across all services
  • Return TraceId in HTTP response headers
  • Correlate logs, traces, and events

Consequences

  • Faster debugging with full end-to-end traceability
  • Improved operability
  • Slight increase in instrumentation complexity
008
YARP Gateway Direct Proxy
Direct HTTP proxying instead of Dapr invocation to avoid header corruption.
Accepted

Context

The YARP gateway initially proxied through Dapr service invocation. Dapr's HTTP-to-gRPC conversion corrupted Authorization headers, and the oauth2clientcredentials middleware replaced user JWTs with machine tokens.

Decision

YARP proxies directly to backend services via Docker DNS, bypassing Dapr for the proxy layer. Dapr sidecar retained for configuration and secrets only.

Lessons Learned

  • Dapr service invocation is not a transparent proxy
  • oauth2clientcredentials middleware overwrites the Authorization header
  • Test auth flows end-to-end in the deployment environment, not just locally
009
AI Multi-Provider Function Calling
OpenAI, Azure OpenAI, Gemini, Claude via Microsoft.Extensions.AI.
Accepted

Context

The AI service needs multiple LLM providers to avoid vendor lock-in, enable cost comparison, and demonstrate provider-agnostic design.

Decision

Adopt Microsoft.Extensions.AI as the provider abstraction. Each provider registered as a keyed singleton IChatClient. Provider selection is runtime-configurable via Redis.

  • FunctionInvokingChatClient handles automatic tool-call loops
  • Topology-aware tool registries (monolith vs microservices) controlled by feature flag
  • Conversations stored in Redis with sliding 40-message window

Consequences

  • Switching providers requires only a config change, not code
  • Unified tool definitions work across all providers
  • Same binary supports both deployment topologies
010
Real-Time AI Notifications (SignalR)
Server-pushed notifications for immediate UI updates during AI tool execution.
Accepted

Context

When AI tools create or modify entities, the user needs immediate feedback beyond the chat response — navigation, data refresh, and visual confirmation.

Decision

SignalR hub at /hubs/notifications with user-scoped groups. Notifications carry W3C Trace Context for end-to-end trace continuity.

Consequences

  • Immediate UI feedback the moment a tool completes
  • End-to-end trace continuity visible in Jaeger
  • Targeted delivery via user-scoped groups
  • Extensible — new notification types require only a new type value
011
Strangler-Fig Connector API & Sagas
Fan-out provisioning across microservices with orchestrated coordination.
Accepted

Context

When a company is created in the monolith, data must be provisioned across multiple microservices with atomicity, idempotency, and observability.

Decision

A dedicated Connector API subscribes to outbox events and orchestrates provisioning via sagas:

  • Fetch data — parallel OData queries to monolith
  • Fan-outTask.WhenAll dispatches to 3 services in parallel
  • Activate — posts results back to monolith
  • Idempotency — Redis state keys with TTLs (120s processing, 7-day completion)

Why Orchestration

  • Activation step depends on results from user-api
  • Error attribution is straightforward
  • Trace spans map naturally to saga phases
012
Reverse Connector (Bidirectional Sync)
Microservice writes synced back to monolith to close the data loop.
Accepted

Context

The forward connector handled monolith-to-microservices flow. But AI chat writes directly to microservices, creating data divergence.

Decision

A reverse-connector-api subscribes to microservice events (micro.* namespace) and replays them to monolith sync endpoints. Uses the same patterns: Dapr pub/sub, Redis idempotency, OpenTelemetry tracing.

Consequences

  • Closed bidirectional data loop — all writes reflected everywhere
  • Original userId forwarded for audit trail
  • Eventual consistency window (typically sub-second)
013
Keycloak Migration
Auth0 to self-hosted Keycloak for cost, control, and operational ownership.
Accepted

Context

Auth0's free tier limits, custom claim complexity, and flat organization model created friction. Self-hosting Keycloak demonstrates infrastructure ownership.

Decision

Hierarchical group-based RBAC replacing Auth0's flat organizations:

  • /Admins — platform administrators
  • /Companies/{uid}/CompanyAdmins — company admin users
  • /Companies/{uid}/Recruiters — recruiter users
  • /Applicants — job seekers

Consequences

  • Hierarchical multi-tenancy without workarounds
  • Full ownership of user management and email templates
  • Group paths are the single source of authorization truth
  • Migration touched ~20 files across 8 services
014
Resume Embedding Pipeline (RAG)
Multi-phase async parsing, pgvector embeddings, and retrieval-augmented generation.
Accepted

Context

Resume parsing involves LLM calls (10-30s). Must not block uploads.

Decision

Three-stage event-driven pipeline via transactional outbox:

  • Stage 1 (Parse): Download blob, extract text, LLM parses in two phases
  • Stage 2 (Embed): Generate batch embeddings for full/skills/experience text (1536-dim)
  • Stage 3 (Delete): Remove embedding when resume deleted

Consequences

  • Non-blocking uploads with streaming progress via SignalR
  • Multi-representation embeddings for targeted retrieval
  • End-to-end traceable from upload through embedding
015
MCP Server Integration
AI tools exposed via Model Context Protocol for dynamic discovery and invocation.
Accepted

Context

In-process AI tools with typed HTTP clients duplicated API knowledge. As the tool surface grew, maintaining duplicate client code became expensive.

Decision

Two MCP servers expose domain tools from the backend:

  • monolith-mcp (port 3333) — backed by monolith database
  • admin-api-mcp (port 3334) — backed by microservices

Both expose identical tool interfaces. The AI service discovers tools at runtime via McpToolProvider.

Consequences

  • Tools live where the logic lives
  • Transparent topology migration via feature flag
  • Full CQRS pipeline enforcement on every tool call
016
Multi-Scope AI Chat
Three chat audiences with separate auth, tools, and system prompts.
Accepted

Context

Three user audiences have different authorization levels, tool access, and conversational goals.

Decision

Three dedicated endpoints with scope-driven tool resolution:

EndpointPolicyGroups
POST /chatAdminChatAdmins
POST /chat/companyCompanyAdminChatAdmins, CompanyAdmins
POST /chat/publicPublicChatAll authenticated

Consequences

  • Least-privilege by design
  • Independent evolution of each scope's tool set
  • Declarative authorization via framework policies
017
IntegrationEvents Shared NuGet
Event schemas packaged as NuGet for consistency across producers and consumers.
Accepted

Context

Multiple services publish and consume integration events. Without a shared source of truth, event schemas drift.

Decision

All event contracts defined in JobBoard.IntegrationEvents and published as a versioned NuGet package.

Consequences

  • Compile-time contract enforcement
  • Single source of truth for event types
  • V1 suffix allows introducing V2 events alongside V1
018
Docker Deployment Topology
Single Linux host with Docker Compose and Dapr sidecars.
Accepted

Context

10+ services with Dapr sidecars on a single Linux host. Kubernetes is not used.

Decision

Docker Compose with service + sidecar pattern. Sidecars use network_mode: "service:{service}" to share the network namespace.

Consequences

  • Single docker compose up -d starts the entire platform
  • Dapr-native sidecar pattern mirrors Kubernetes
  • Dev and prod compose files are structurally identical
019
EF Core Dual-ID Pattern
Integer PK for DB performance + GUID for API exposure.
Accepted

Context

Every entity needs an integer PK for database performance and a GUID for API responses.

Decision

Dual-ID base entity: int InternalId (DB primary key) + Guid Id (public API identifier via Guid.CreateVersion7()).

Consequences

  • Performant clustered indexes with compact integer keys
  • Secure API surface — only GUIDs exposed externally
  • Pre-insert ID knowledge enables complex aggregate creation
020
Aspire Local Orchestration
.NET Aspire launches 36 resources with dependency ordering and health checks.
Accepted

Context

The local environment previously required manually launching 10+ services, 8 Dapr sidecars, Docker Compose for infrastructure, and Angular dev servers.

Decision

A single Aspire AppHost orchestrates ~36 resources with one command. Includes a Seed Runner container for Redis, SQL Server, and PostgreSQL seeding.

Consequences

  • Single-command startup
  • Dependency ordering with deterministic seeding
  • Unified dashboard with logs, traces, and health
  • .NET projects run as native processes (debugger-friendly)