A scalable, microservice-based article platform built with .NET 10.0 and Clean Architecture principles. Demonstrates geographical sharding, independent microservices, containerization, and centralized observability.
- X-Axis (Horizontal Duplication): Multiple API instances per service behind an Nginx load balancer. ArticleService: ports 8081–8083. DraftService: ports 8084–8086.
- Y-Axis (Functional Decomposition):
ArticleServicehandles published articles and comments.DraftServicehandles author drafts. Each has its own database, Dockerfile, and deployment lifecycle. - Z-Axis (Data Partitioning): Articles are sharded across 8 continent-based PostgreSQL databases for data locality and regional scaling.
- Geographical Sharding: Articles distributed across 8 continent-based PostgreSQL shards
- Independent Microservices: ArticleService and DraftService with separate DBs and containers
- Clean Architecture: Domain / Application / Infrastructure / API layers enforced by project boundaries
- Centralized Observability: Shared
HappyHeadlines.Observabilitylibrary wires Serilog + OpenTelemetry into any service with one method call - Structured Logging + Tracing: Seq aggregates logs and traces from all services with automatic retention and sensitive data scrubbing
- Correlation ID: Every request carries an
X-Correlation-IDheader threaded through all log events and trace spans - Containerization: Full Docker Compose stack started with one command
- Load Balancing: Nginx with separate upstreams for each service
- API Documentation: Scalar/OpenAPI available on each service in development
client → nginx :5000
├── /api/drafts → draft-api-1/2/3 (DraftService DB :5442)
└── / → article-api-1/2/3 (8 shard DBs :5432-5439, comments :5440, profanity :5441)
↓
Seq :5380 (logs + traces from all services)Single library consumed by every service. Exposes two extension methods:
builder.AddHappyHeadlinesObservability("service-name")— wires Serilog + OpenTelemetryapp.UseHappyHeadlinesObservability()— registers CorrelationId middleware + request logging
| Layer | Project | Responsibility |
|---|---|---|
| Domain | ArticleService.Domain |
Article, Comment, ProfaneWord entities |
| Application | ArticleService.Application |
Services, interfaces, DTOs, circuit breaker |
| Infrastructure | ArticleService.Infrastructure |
EF Core, repositories, continent shard router |
| API | ArticleService.Api |
Minimal API endpoints, DI wiring |
Databases: 8 continent shards (ports 5432–5439), Comments DB (5440), Profanity DB (5441)
| Layer | Project | Responsibility |
|---|---|---|
| Domain | DraftService.Domain |
Draft entity |
| Application | DraftService.Application |
DraftAppService, IDraftRepository, DTOs, Result<T> |
| Infrastructure | DraftService.Infrastructure |
EF Core DbContext, DraftRepository |
| API | DraftService.Api |
Minimal API endpoints, DI wiring |
Database: Single drafts DB (port 5442)
- Backend: .NET 10.0, ASP.NET Core Minimal APIs
- Database: PostgreSQL 16 with Entity Framework Core (Npgsql)
- Logging / Tracing: Serilog, OpenTelemetry (OTLP), Seq
- Containerization: Docker, Docker Compose
- Load Balancing: Nginx
- Documentation: Scalar/OpenAPI
docker-compose up --build -d| Container | Purpose | Port |
|---|---|---|
nginx |
Load balancer entry point | 5000 |
article-api-1/2/3 |
ArticleService replicas | 8081–8083 |
draft-api-1/2/3 |
DraftService replicas | 8084–8086 |
seq |
Log & trace UI | 5380 (UI), 5341 (ingest) |
db-africa … db-global |
Article continent shards | 5432–5439 |
db-comment |
Comments database | 5440 |
db-profanity |
Profanity filter database | 5441 |
db-draft |
Drafts database | 5442 |
Start only infrastructure, then dotnet run the service you're working on. appsettings.Development.json in each service overrides all connection strings to localhost with the exposed Docker ports.
docker-compose up -d seq db-africa db-antarctica db-asia db-europe db-northamerica db-oceania db-southamerica db-global db-comment db-profanity db-draft
cd src/DraftService/DraftService.Api && dotnet run| URL | What you see |
|---|---|
http://localhost:5380 |
Seq — all structured logs and traces from every service |
http://localhost:3000 |
Grafana — pre-provisioned cache dashboard (admin / admin) |
http://localhost:8081/scalar |
ArticleService OpenAPI (replica 1) |
http://localhost:8084/scalar |
DraftService OpenAPI (replica 1) |
Use the script below to generate cache traffic and print proof details for both Seq and Grafana.
.\scripts\demo-observability.ps1What the script does:
- Starts the Docker Compose stack if needed
- Generates Article cache misses and hits
- Generates Comment cache miss and hits
- Waits for Prometheus scrape and prints cache metric values
- Prints ready-to-paste Seq filters and dashboard hints
Use these Seq filters (also printed by the script):
ServiceName = 'article-service' and (@Message like '%Cache MISS for article%' or @Message like '%Cache HIT for article%' or @Message like '%not in cache%' or @Message like '%served from cache%')ServiceName = 'comment-service' and (@Message like '%Comment cache MISS%' or @Message like '%Comment cache HIT%' or @Message like '%not in cache%' or @Message like '%served from cache%')
For Grafana, open HappyHeadlines - Cache Hit Ratios and set time range to Last 15 minutes.
Log levels: Debug (queries, dev only) → Information (business events) → Warning (not found, validation) → Error (exceptions, DB failures). Passwords, tokens, and PII are automatically redacted by SensitivePropertyScrubber before any log event leaves the process.
Via nginx http://localhost:5000
| Method | Route | Description |
|---|---|---|
POST |
/api/articles |
Create article |
GET |
/api/articles/{id}?continent={c} |
Get article by ID |
PUT |
/api/articles/{id} |
Update article |
DELETE |
/api/articles/{id} |
Delete article |
POST |
/api/comments |
Create comment (profanity-checked) |
GET |
/api/comments/{id} |
Get comment by ID |
GET |
/api/articles/{id}/comments |
Get all comments for an article |
POST |
/api/profanity |
Add profanity word |
GET |
/api/profanity |
List all profanity words |
Via nginx http://localhost:5000
| Method | Route | Description |
|---|---|---|
POST |
/api/drafts |
Save new draft |
GET |
/api/drafts/{id} |
Get draft by ID |
GET |
/api/drafts/author/{authorId} |
Get all drafts by author |
PUT |
/api/drafts/{id} |
Update draft |
DELETE |
/api/drafts/{id} |
Delete draft |


