Clean Architecture · Quick recall Q&A

6 min read
Mid-level13 min read
Rapid overview

Quick recall Q&A

Q: What is the Dependency Rule in Clean Architecture? A: The Dependency Rule states that source code dependencies must point only inward. Outer layers (frameworks, UI, database) can depend on inner layers (use cases, entities), but never the reverse. This ensures that business rules are isolated from external concerns and can be tested, deployed, and evolved independently.

Q: How does Clean Architecture differ from traditional N-tier (3-layer) architecture? A: In N-tier, the data layer is typically at the bottom and everything depends on it. In Clean Architecture, the domain is at the center and infrastructure depends on the domain (via interfaces). The dependency direction is inverted. This means you can swap databases or frameworks without touching business logic.

Q: Where should interfaces for repositories be defined? A: In the Application layer (or Domain layer if they represent a core domain concept). The implementations live in Infrastructure. This ensures Application code depends only on abstractions, and Infrastructure details can be swapped out without modifying use cases.

Q: Explain how Dependency Injection supports Clean Architecture. A: DI is the runtime mechanism that connects abstractions (defined in inner layers) with their concrete implementations (defined in outer layers). At the composition root (typically Program.cs), you register ITradeRepository to be resolved as TradeRepository. This allows inner layers to code against interfaces while the container provides the right implementation at runtime.

Q: What is the role of the Application layer? A: The Application layer contains application-specific business rules. It orchestrates the flow of data between the Presentation and Domain layers. It holds use case handlers (CQRS commands/queries), validators, DTOs, and interface definitions for infrastructure services. It depends on Domain but never on Infrastructure or Presentation.

Q: Why separate Commands and Queries (CQRS)? A: Separating reads and writes lets you optimize each independently. Commands go through the full domain model with validation and invariant enforcement. Queries can use lightweight read models or projections that skip the domain entirely, improving performance. It also clarifies intent -- a developer reading the code immediately knows whether a handler modifies state.

Q: How do you handle cross-cutting concerns like validation and logging in Clean Architecture? A: Use MediatR pipeline behaviors. A ValidationBehavior<TRequest, TResponse> runs all registered IValidator<TRequest> implementations before the handler executes. A LoggingBehavior can log request/response details. This keeps handlers focused on business logic and avoids duplicating cross-cutting code.

Q: What is the Unit of Work pattern and why use it with repositories? A: Unit of Work tracks all changes made during a business transaction and commits them atomically. In EF Core, DbContext already implements this pattern. An explicit IUnitOfWork interface in the Application layer gives handlers control over when changes are saved and allows dispatching domain events before or after the commit.

Q: Should domain entities have public setters? A: No. Domain entities should protect their invariants using private setters, factory methods, and behavior methods. Public setters allow any code to put the entity into an invalid state, bypassing business rules. Use methods like trade.Close(exitPrice) that encapsulate the state transition and enforce invariants.

Q: How do you test a use case handler without a real database? A: Mock the repository and unit of work interfaces using a framework like Moq or NSubstitute. Pass the mocks into the handler's constructor. Assert that the handler calls the expected repository methods with the correct arguments and that it calls CommitAsync on the unit of work. Domain logic is tested separately in domain unit tests.

Q: How do you prevent domain entities from leaking into API responses? A: Use DTOs (Data Transfer Objects) as the return type from handlers. Map domain entities to DTOs in the handler or via AutoMapper/Mapster. Controllers receive DTOs and optionally map them to ViewModels or API response objects. Never serialize domain entities directly -- they contain internal state (like domain events) that should not be exposed.

Q: When would you relax strict Clean Architecture layering? A: When profiling proves that the indirection causes a measurable performance bottleneck (rare in practice), or for simple CRUD operations that have no meaningful business logic. Even then, document the decision and limit the scope. For read-only queries, skipping the domain model in favor of direct projections is a common and accepted optimization.

Q: How does Clean Architecture relate to Domain-Driven Design (DDD)? A: They complement each other. DDD shapes the Domain layer -- it provides patterns like entities, value objects, aggregates, domain events, and bounded contexts. Clean Architecture provides the surrounding structure -- it defines how dependencies flow, where interfaces live, and how layers interact. You can use Clean Architecture without DDD, but DDD enriches the domain model.

Q: How do you handle exceptions across layers? A: Define custom exception types per layer. The Domain layer throws DomainException for invariant violations. The Application layer throws NotFoundException or ValidationException. A global exception-handling middleware in the Presentation layer catches these and maps them to appropriate HTTP status codes (422 for domain errors, 404 for not found, 400 for validation, 500 for unexpected errors).

Q: What is the composition root and where does it belong? A: The composition root is the single place where all DI bindings are configured. In ASP.NET Core, it is in Program.cs (or a set of Add* extension methods called from there). It belongs in the outermost layer (Presentation/API) because it must know about all layers to wire them together. This is the only place where the Presentation project directly references Infrastructure.

Q: How do you enforce architectural rules in CI/CD? A: Use NetArchTest to write architecture tests that verify dependency rules at build time. For example, assert that MyApp.Domain does not reference MyApp.Infrastructure. Run these tests as part of the CI pipeline. If a developer adds a forbidden reference, the test fails and the build breaks.

Q: What are the trade-offs of Clean Architecture? A: Benefits include testability, flexibility, and independence from frameworks. Trade-offs include more files and indirection (more projects, more interfaces, more mapping). For small CRUD applications, it can feel like over-engineering. The architecture pays off as the application grows, the team scales, and requirements change frequently. Start simple and grow into full Clean Architecture as complexity demands it.

Q: How do MediatR notifications relate to domain events in Clean Architecture? A: Domain events are raised by entities when meaningful state changes occur. In the Unit of Work, before or after SaveChangesAsync, you collect these events and publish them as MediatR INotification messages. Event handlers (in the Application layer) can then trigger side effects like sending emails, updating read models, or publishing integration events to a message broker -- all without the domain entity knowing about these concerns.

Q: Should every project use Clean Architecture? A: No. Clean Architecture is best suited for applications with complex or evolving business logic, multiple teams, or long lifespans. For simple APIs, prototypes, or microservices with minimal logic, a simpler layered or vertical-slice approach may be more pragmatic. Choose the architecture that fits the complexity of the problem.

Trades/
  Commands/
    OpenTrade/
      OpenTradeCommand.cs
      OpenTradeHandler.cs
      OpenTradeValidator.cs
    CloseTrade/
      CloseTradeCommand.cs
      CloseTradeHandler.cs
      CloseTradeValidator.cs
  Queries/
    GetTradeById/
      GetTradeByIdQuery.cs
      GetTradeByIdHandler.cs
      TradeDto.cs
Q: How do you organize code within the Application layer -- by feature or by type? A: Organize by feature (vertical slices). Group the command, handler, validator, and DTO for a use case together in a folder:

This keeps related code together, reduces navigation, and makes it easy to find everything related to a use case. Avoid grouping all commands in one folder, all handlers in another, and all validators in a third -- that scatters related code across the project.

See also