Skip to main content

Command Palette

Search for a command to run...

What Happens Under the Hood: The Journey to abdullahhakim.me (Part 2)

Written by
Abdullah Hakim
Published on
--
Views
212
What Happens Under the Hood: The Journey to abdullahhakim.me (Part 2)

Inside the .NET 10 Engine: Clean Architecture & CQRS

In Part 1, we followed the journey of a request from your browser to my server. We saw how DNS, TLS, Nginx, and Docker containers work together to route traffic securely and efficiently. But routing is only half the story. Once that request hits the ASP.NET Core Web API, it enters an ecosystem designed for clarity, testability, and scale.

This post is about the brain of Hakim: the .NET backend. Specifically, how I structured it using Clean Architecture and CQRS (Command Query Responsibility Segregation) via MediatR — and why this isn't just enterprise over-engineering, but a deliberate choice that makes a personal platform feel like production software.


Why Clean Architecture?

If you've ever worked on a monolith where business logic leaks into controllers, or where a database schema change breaks half the application, you already know the pain. Traditional N-layer architecture (Presentation → Business → Data) is easy to start, but dependencies quickly become a tangled mess.

Clean Architecture (popularized by Robert C. Martin) inverts this. The rule is simple:

The Domain layer knows nothing about the database, the UI, or the framework.

Dependencies point inward. The center is pure business logic; the outer layers are infrastructure concerns.

Here is how I structured the Hakim backend:

plaintext

  • Hakim.Domain: Zero NuGet dependencies. Just plain C# classes, enums, and logic. It doesn't know Entity Framework exists.

  • Hakim.Application: Orchestrates use cases via MediatR. Defines interfaces (e.g., IAppDbContext, ICacheService) that infrastructure must implement.

  • Hakim.Infrastructure: The "dirty" layer. Talks to PostgreSQL via EF Core and caches to Redis.

  • Hakim.Api: The thin shell. Controllers only deserialize HTTP requests and dispatch them via MediatR. No business logic lives here.

This strict separation means I can swap database providers or cache strategies without touching a single line of domain code.

CQRS: Separating Reads from Writes

In a standard CRUD API, the same model is often used for creating, updating, and reading data. That works until it doesn't. Reads and writes have fundamentally different needs:

  • Queries need to be fast, often denormalized, and cache-friendly.

  • Commands need validation, business rules, and transactional integrity.

CQRS splits them. In Hakim, every API operation is either a Command (mutates state) or a Query (returns data). MediatR acts as the dispatcher.

Let's look at both sides in practice.


The Query Side: Reading a Blog Post

When you load a post, the frontend queries the API. Notice how thin the controller is, relying on [ETag] for browser-level caching and MediatR for the heavy lifting:

csharp

The controller has no idea how to fetch a post. It simply builds a GetPostBySlugQuery and dispatches it.

The handler is where the performance magic happens:

csharp

Why this matters:

  1. Compiled EF Core Queries: EF Core compiles the expression once and reuses the execution plan, shaving milliseconds off every database call.

  2. Redis as a Read Replica: For a blog post that receives traffic spikes, the database is hit once every ten minutes. Every other request is served from memory in microseconds.

  3. Value Objects: Notice new Slug(request.Slug). This ensures that slug validation and formatting rules are encapsulated in the domain, not scattered across queries.


The Command Side: Creating a Post

Queries are straightforward. Commands are where the business rules live. Because my blog supports both English and Arabic, the CreatePostCommand accepts localized translations:

csharp

And the handler ensures data integrity and cache invalidation:

csharp

Notice how the write operation explicitly handles side effects like removing stale list data from Redis (RemoveAsync), ensuring the read side stays eventually consistent.


Pipeline Behaviors: The Secret Sauce

One of the most powerful features of MediatR is Pipeline Behaviors. These act like middleware that wraps around every request handler, keeping the core logic pristine.

In DependencyInjection.cs, I register a global ValidationBehavior:

csharp

Before any command handler runs, FluentValidation checks the request. If validation fails, a ValidationException is thrown and the handler never executes.

csharp

This is the power of the pipeline: handlers stay focused entirely on domain operations, while infrastructure and validation concerns are composed around them.


Performance in Practice

You might wonder: does all this abstraction actually matter for a personal blog?

On a cold start, a blog post query from PostgreSQL takes ~45ms. With the compiled query and Redis cache, subsequent requests drop to ~2ms. The command pipeline ensures that I can't accidentally insert an invalid slug or publish a post without a valid culture code.

More importantly, the architecture scales horizontally. If Hakim ever needs to handle 10,000 concurrent readers, I can spin up more API containers behind Nginx without rewriting a single handler. The state lives in PostgreSQL and Redis; the application layer is stateless.


What's Next?

In Part 1, we traced the highway. In Part 2, we opened the engine.

But Hakim isn't just a blog. Under the hood, there is an entirely separate microservice that watches job boards, scrapes listings, and uses an LLM to tailor my resume in real-time — all triggered by a background worker running inside the same Docker host.

In Part 3, I'll pull back the curtain on the AI-driven resume tailoring pipeline: how the FastAPI microservice scrapes listings, how the .NET background service orchestrates the workflow, and how Groq + Reactive Resume API generate a tailored PDF and dispatch it via SMTP — fully automated, fully decoupled.

Until then, thanks for reading.

Last updated: --
What Happens Under the Hood: The Journey to abdullahhakim.me (Part 2) | Abdullah Hakim