Round 4

Take-Home Project — Clean Architecture Guide

Your complete roadmap for building production-grade backend systems using Clean Architecture (Onion/Hexagonal). Learn what evaluators look for, common pitfalls, and strategies to impress senior developers during code review.

2
Weeks Timeline
8
Core Areas
100%
Production Ready
15
Key Checklist Items
01

What to Expect

Understanding the round format helps you manage scope and time effectively. This isn't a test—it's a collaborative signal of how you approach real-world problems.

Timeline & Scope

You have 2 weeks to build a small-to-medium backend project. Typical scope:

What Gets Evaluated

The Code Review Meeting

After submission, you'll meet with a senior developer for ~1 hour:

ℹ️
Pro tip: Evaluators value thoughtful trade-off decisions over "perfect" implementations. If you chose simple DI over a heavyweight IoC container, be ready to justify it.
02

Project Structure: Clean Architecture

Clean Architecture (Onion Model) separates concerns into independent, testable layers. Each layer has a single responsibility and depends only on inner layers.

The Four-Layer Model

Layer 1: Domain (Core)

Pure business logic with zero infrastructure dependencies. No HTTP, no databases—just entities and interfaces.

csharp
// Domain/Entities/User.cs - Pure business entity
public class User {
    public int Id { get; set; }
    public string Email { get; set; }
    public string FullName { get; set; }
    public bool IsActive { get; set; }
    public class CreateCommand {
        public string Email { get; set; }
        public string FullName { get; set; }
    }
}

// Domain/Interfaces/IUserRepository.cs - Contracts (no impl)
public interface IUserRepository {
    Task<User> GetByIdAsync(int id);
    Task<User> GetByEmailAsync(string email);
    Task<int> CreateAsync(User user);
}

Layer 2: Application (Use Cases)

Orchestrates domain entities to execute business use cases. Contains DTOs, validators, and application services.

csharp
// Application/UseCases/CreateUserHandler.cs
public class CreateUserHandler {
    private readonly IUserRepository _userRepository;
    private readonly IPasswordHasher _passwordHasher;
    private readonly IValidator<User.CreateCommand> _validator;

    public CreateUserHandler(IUserRepository userRepository, IPasswordHasher hasher, IValidator<User.CreateCommand> validator) {
        _userRepository = userRepository;
        _passwordHasher = hasher;
        _validator = validator;
    }

    public async Task<int> HandleAsync(User.CreateCommand command) {
        // Validate input
        var validationResult = await _validator.ValidateAsync(command);
        if (!validationResult.IsValid) {
            throw new ValidationException(validationResult.Errors);
        }

        // Check if email already exists
        var existing = await _userRepository.GetByEmailAsync(command.Email);
        if (existing != null) {
            throw new DomainException("Email already registered");
        }

        // Create and persist
        var user = new User {
            Email = command.Email,
            FullName = command.FullName,
            IsActive = true
        };
        return await _userRepository.CreateAsync(user);
    }
}

// Application/DTOs/UserResponseDto.cs
public class UserResponseDto {
    public int Id { get; set; }
    public string Email { get; set; }
    public string FullName { get; set; }
}

Layer 3: Infrastructure

Implements repository interfaces, database access, and external service integrations. Uses EF Core, HTTP clients, etc.

csharp
// Infrastructure/Data/EfUserRepository.cs
public class EfUserRepository : IUserRepository {
    private readonly ApplicationDbContext _dbContext;

    public EfUserRepository(ApplicationDbContext dbContext) {
        _dbContext = dbContext;
    }

    public async Task<User> GetByIdAsync(int id) {
        return await _dbContext.Users.FirstOrDefaultAsync(u => u.Id == id);
    }

    public async Task<User> GetByEmailAsync(string email) {
        return await _dbContext.Users.FirstOrDefaultAsync(u => u.Email == email);
    }

    public async Task<int> CreateAsync(User user) {
        _dbContext.Users.Add(user);
        await _dbContext.SaveChangesAsync();
        return user.Id;
    }
}

// Infrastructure/Data/ApplicationDbContext.cs
public class ApplicationDbContext : DbContext {
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) {}

    public DbSet<User> Users { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder) {
        base.OnModelCreating(modelBuilder);
        modelBuilder.ApplyConfigurationsFromAssembly(typeof(ApplicationDbContext).Assembly);
    }
}

Layer 4: API (Presentation)

HTTP controllers, middleware, and Swagger docs. Depends on all inner layers.

csharp
// API/Controllers/UsersController.cs
[ApiController]
[Route("api/users")]
public class UsersController : ControllerBase {
    private readonly CreateUserHandler _createUserHandler;
    private readonly ILogger<UsersController> _logger;

    public UsersController(CreateUserHandler createUserHandler, ILogger<UsersController> logger) {
        _createUserHandler = createUserHandler;
        _logger = logger;
    }

    [HttpPost]
    public async Task<IActionResult> CreateUserAsync([FromBody] User.CreateCommand command) {
        try {
            _logger.LogInformation("Creating user: {Email}", command.Email);
            var userId = await _createUserHandler.HandleAsync(command);
            return CreatedAtAction(nameof(GetUserAsync), new { id = userId }, new { id = userId });
        } catch (ValidationException ex) {
            _logger.LogWarning("Validation failed: {Message}", ex.Message);
            return BadRequest(new { errors = ex.Errors });
        } catch (DomainException ex) {
            _logger.LogWarning("Domain error: {Message}", ex.Message);
            return Conflict(new { message = ex.Message });
        }
    }

    [HttpGet("{id:int}")]
    public async Task<IActionResult> GetUserAsync(int id) {
        // Implementation
        return Ok();
    }
}

Folder Structure

ProjectName/ ├── Domain/ │ ├── Entities/ │ │ ├── User.cs │ │ ├── Product.cs │ │ └── Order.cs │ ├── Interfaces/ │ │ ├── IUserRepository.cs │ │ ├── IProductRepository.cs │ │ ├── IPasswordHasher.cs │ │ └── IEmailService.cs │ └── Exceptions/ │ └── DomainException.cs ├── Application/ │ ├── UseCases/ │ │ ├── Users/ │ │ │ ├── CreateUserHandler.cs │ │ │ ├── GetUserHandler.cs │ │ │ └── UpdateUserHandler.cs │ │ ├── Products/ │ │ │ └── ListProductsHandler.cs │ │ └── Orders/ │ │ └── CreateOrderHandler.cs │ ├── DTOs/ │ │ ├── UserResponseDto.cs │ │ ├── ProductResponseDto.cs │ │ └── OrderResponseDto.cs │ ├── Validators/ │ │ ├── CreateUserValidator.cs │ │ └── CreateOrderValidator.cs │ └── Exceptions/ │ └── ValidationException.cs ├── Infrastructure/ │ ├── Data/ │ │ ├── ApplicationDbContext.cs │ │ ├── EfUserRepository.cs │ │ ├── EfProductRepository.cs │ │ ├── EntityConfigurations/ │ │ │ ├── UserConfiguration.cs │ │ │ └── OrderConfiguration.cs │ │ └── Migrations/ │ └── Services/ │ ├── PasswordHasher.cs │ ├── EmailService.cs │ └── ExternalApiClient.cs ├── API/ │ ├── Controllers/ │ │ ├── UsersController.cs │ │ ├── ProductsController.cs │ │ └── OrdersController.cs │ ├── Middleware/ │ │ ├── ErrorHandlingMiddleware.cs │ │ └── LoggingMiddleware.cs │ ├── Program.cs │ ├── Dockerfile │ └── appsettings.json ├── Tests/ │ ├── Unit/ │ │ ├── CreateUserHandlerTests.cs │ │ └── UserValidatorTests.cs │ └── Integration/ │ ├── UsersControllerTests.cs │ └── OrdersControllerTests.cs ├── docker-compose.yml └── README.md
03

Must-Have Features

These features distinguish production-grade code from student projects. Evaluators expect all of them.

1. Dependency Injection (IServiceCollection)

Configure all dependencies in one place. Makes testing and scaling straightforward.

csharp
// Program.cs - DI Registration
var builder = WebApplication.CreateBuilder(args);

// Database
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))
);

// Repositories
builder.Services.AddScoped<IUserRepository, EfUserRepository>();
builder.Services.AddScoped<IProductRepository, EfProductRepository>();

// Use Cases
builder.Services.AddScoped<CreateUserHandler>();
builder.Services.AddScoped<GetUserHandler>();

// Services
builder.Services.AddScoped<IPasswordHasher, BcryptPasswordHasher>();
builder.Services.AddScoped<IEmailService, SmtpEmailService>();

// Validators
builder.Services.AddValidatorsFromAssembly(typeof(Program).Assembly);

// Controllers & Logging
builder.Services.AddControllers();
builder.Services.AddLogging(config =>
    config.AddConsole().AddDebug()
);

var app = builder.Build();
app.UseHttpsRedirection();
app.MapControllers();
await app.RunAsync();

2. Repository Pattern with Unit of Work

Abstracts data access, enables testing, and maintains consistency across entities.

csharp
// Domain/Interfaces/IUnitOfWork.cs
public interface IUnitOfWork : IDisposable {
    IUserRepository Users { get; }
    IProductRepository Products { get; }
    IOrderRepository Orders { get; }
    Task<int> SaveChangesAsync();
}

// Infrastructure/Data/UnitOfWork.cs
public class UnitOfWork : IUnitOfWork {
    private readonly ApplicationDbContext _dbContext;
    private IUserRepository _userRepository;
    private IProductRepository _productRepository;

    public UnitOfWork(ApplicationDbContext dbContext) {
        _dbContext = dbContext;
    }

    public IUserRepository Users => _userRepository ??= new EfUserRepository(_dbContext);
    public IProductRepository Products => _productRepository ??= new EfProductRepository(_dbContext);

    public async Task<int> SaveChangesAsync() => await _dbContext.SaveChangesAsync();

    public void Dispose() => _dbContext?.Dispose();
}

3. Unit Tests with xUnit + Moq

Test business logic in isolation. Aim for >70% code coverage.

csharp
// Tests/Unit/CreateUserHandlerTests.cs
public class CreateUserHandlerTests {
    private readonly Mock<IUserRepository> _userRepoMock;
    private readonly Mock<IPasswordHasher> _hasherMock;
    private readonly CreateUserHandler _handler;

    public CreateUserHandlerTests() {
        _userRepoMock = new Mock<IUserRepository>();
        _hasherMock = new Mock<IPasswordHasher>();
        _handler = new CreateUserHandler(_userRepoMock.Object, _hasherMock.Object);
    }

    [Fact]
    public async Task Handle_WithValidCommand_ReturnsUserId() {
        // Arrange
        var command = new User.CreateCommand {
            Email = "john@example.com",
            FullName = "John Doe"
        };
        _userRepoMock.Setup(r => r.GetByEmailAsync(command.Email)).ReturnsAsync((User)null);
        _userRepoMock.Setup(r => r.CreateAsync(It.IsAny<User>())).ReturnsAsync(1);

        // Act
        var result = await _handler.HandleAsync(command);

        // Assert
        Assert.Equal(1, result);
        _userRepoMock.Verify(r => r.CreateAsync(It.IsAny<User>()), Times.Once);
    }

    [Fact]
    public async Task Handle_WithExistingEmail_ThrowsDomainException() {
        // Arrange
        var command = new User.CreateCommand { Email = "existing@example.com" };
        _userRepoMock.Setup(r => r.GetByEmailAsync(command.Email))
            .ReturnsAsync(new User { Id = 1, Email = command.Email });

        // Act & Assert
        await Assert.ThrowsAsync<DomainException>(() => _handler.HandleAsync(command));
    }
}

4. Integration Tests

Test API endpoints end-to-end with a real (test) database.

csharp
// Tests/Integration/UsersControllerTests.cs
public class UsersControllerTests {
    private readonly WebApplicationFactory<Program> _factory;
    private readonly HttpClient _client;

    public UsersControllerTests() {
        _factory = new WebApplicationFactory<Program>()
            .WithWebHostBuilder(builder => {
                builder.ConfigureServices(services => {
                    // Replace real DB with in-memory
                    var descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions<ApplicationDbContext>));
                    services.Remove(descriptor);
                    services.AddDbContext<ApplicationDbContext>(options =>
                        options.UseInMemoryDatabase("TestDb")
                    );
                });
            });
        _client = _factory.CreateClient();
    }

    [Fact]
    public async Task CreateUser_WithValidData_Returns201() {
        // Arrange
        var payload = new { Email = "test@example.com", FullName = "Test User" };
        var content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");

        // Act
        var response = await _client.PostAsync("/api/users", content);

        // Assert
        Assert.Equal(HttpStatusCode.Created, response.StatusCode);
    }
}

5. Error Handling Middleware

Centralize exception handling to return consistent HTTP responses.

csharp
// API/Middleware/ErrorHandlingMiddleware.cs
public class ErrorHandlingMiddleware {
    private readonly RequestDelegate _next;
    private readonly ILogger<ErrorHandlingMiddleware> _logger;

    public ErrorHandlingMiddleware(RequestDelegate next, ILogger<ErrorHandlingMiddleware> logger) {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context) {
        try {
            await _next(context);
        } catch (Exception ex) {
            _logger.LogError(ex, "Unhandled exception");
            context.Response.ContentType = "application/json";

            var response = ex switch {
                ValidationException ve => new { statusCode = 400, message = "Validation failed", errors = ve.Errors },
                DomainException de => new { statusCode = 409, message = de.Message },
                _ => new { statusCode = 500, message = "Internal server error" }
            };

            context.Response.StatusCode = (int)(dynamic)response.GetType().GetProperty("statusCode").GetValue(response);
            await context.Response.WriteAsJsonAsync(response);
        }
    }
}

6. Logging (ILogger)

Instrument code for debugging and monitoring. Use structured logging.

csharp
// Infrastructure/Services/UserService.cs - With Logging
public class UserService {
    private readonly IUserRepository _repository;
    private readonly ILogger<UserService> _logger;

    public UserService(IUserRepository repository, ILogger<UserService> logger) {
        _repository = repository;
        _logger = logger;
    }

    public async Task<User> GetUserAsync(int id) {
        _logger.LogInformation("Fetching user: {UserId}", id);
        var user = await _repository.GetByIdAsync(id);

        if (user == null) {
            _logger.LogWarning("User not found: {UserId}", id);
            throw new NotFoundException("User not found");
        }

        _logger.LogInformation("User retrieved successfully: {UserId} {Email}", user.Id, user.Email);
        return user;
    }
}

7. Input Validation (FluentValidation)

Validate data at the application layer before business logic.

csharp
// Application/Validators/CreateUserValidator.cs
public class CreateUserValidator : AbstractValidator<User.CreateCommand> {
    public CreateUserValidator() {
        RuleFor(x => x.Email)
            .NotEmpty().WithMessage("Email is required")
            .EmailAddress().WithMessage("Email must be valid")
            .MaximumLength(255).WithMessage("Email is too long");

        RuleFor(x => x.FullName)
            .NotEmpty().WithMessage("Full name is required")
            .Length(2, 100).WithMessage("Full name must be 2-100 chars");
    }
}

8. Swagger/OpenAPI Documentation

Auto-generate and document your API.

csharp
// Program.cs - Swagger Setup
builder.Services.AddSwaggerGen(c => {
    c.SwaggerDoc("v1", new OpenApiInfo {
        Title = "My API",
        Version = "v1",
        Description = "Production API with Clean Architecture"
    });
});

var app = builder.Build();
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "v1"));

// Controller - With XML Documentation
[HttpPost]
[ProducesResponseType(typeof(UserResponseDto), 201)]
[ProducesResponseType(400)]
/// <summary>
/// Creates a new user account</summary>
/// <param name="command">User creation data</param>
public async Task<IActionResult> CreateAsync([FromBody] User.CreateCommand command) {
    // Implementation
}

9. Docker Support

Containerize your application for easy deployment.

dockerfile
# Dockerfile
FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
WORKDIR /app
COPY . .
RUN dotnet restore
RUN dotnet publish -c Release -o out

FROM mcr.microsoft.com/dotnet/aspnet:7.0
WORKDIR /app
COPY --from=build /app/out .
EXPOSE 8080
ENTRYPOINT ["dotnet", "API.dll"]
yaml
# docker-compose.yml
version: '3.8'
services:
  api:
    build: .
    ports:
      - "8080:8080"
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
      - ConnectionStrings__DefaultConnection=Server=db;Database=myapp;User Id=sa;Password=YourPassword123!
    depends_on:
      - db
  db:
    image: mcr.microsoft.com/mssql/server:2022-latest
    environment:
      ACCEPT_EULA: "Y"
      SA_PASSWORD: YourPassword123!
    ports:
      - "1433:1433"
04

Code Quality Checklist

Before you submit, verify every item. This is what evaluators assess in code review.

SOLID Principles

Naming & Consistency

Async/Await

Magic Numbers & Strings

HTTP Status Codes

Git History

Testing

Error Handling

Documentation

05

Example: Simple E-Commerce Project

A real end-to-end example showing proper architecture, testing, and deployment setup.

Domain Layer: Core Entities

csharp
// Domain/Entities/Product.cs
public class Product {
    public int Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public decimal Price { get; set; }
    public int StockQuantity { get; set; }
    public DateTime CreatedAt { get; set; }

    // Business method: Reduce stock on purchase
    public void ReduceStock(int quantity) {
        if (quantity > StockQuantity) {
            throw new DomainException("Insufficient stock");
        }
        StockQuantity -= quantity;
    }

    public class CreateCommand {
        public string Name { get; set; }
        public string Description { get; set; }
        public decimal Price { get; set; }
        public int StockQuantity { get; set; }
    }
}

// Domain/Interfaces/IProductRepository.cs
public interface IProductRepository {
    Task<Product> GetByIdAsync(int id);
    Task<List<Product>> ListAsync(int skip = 0, int take = 10);
    Task<int> CreateAsync(Product product);
    Task<bool> UpdateAsync(Product product);
}

Application Layer: Use Case Handler

csharp
// Application/UseCases/CreateProductHandler.cs
public class CreateProductHandler {
    private readonly IProductRepository _repository;
    private readonly IValidator<Product.CreateCommand> _validator;
    private readonly ILogger<CreateProductHandler> _logger;

    public CreateProductHandler(
        IProductRepository repository,
        IValidator<Product.CreateCommand> validator,
        ILogger<CreateProductHandler> logger) {
        _repository = repository;
        _validator = validator;
        _logger = logger;
    }

    public async Task<int> HandleAsync(Product.CreateCommand command) {
        // Validate
        var result = await _validator.ValidateAsync(command);
        if (!result.IsValid) {
            _logger.LogWarning("Product validation failed");
            throw new ValidationException(result.Errors);
        }

        // Create entity
        var product = new Product {
            Name = command.Name,
            Description = command.Description,
            Price = command.Price,
            StockQuantity = command.StockQuantity,
            CreatedAt = DateTime.UtcNow
        };

        // Persist
        var id = await _repository.CreateAsync(product);
        _logger.LogInformation("Product created: {ProductId}", id);
        return id;
    }
}

// Application/Validators/CreateProductValidator.cs
public class CreateProductValidator : AbstractValidator<Product.CreateCommand> {
    public CreateProductValidator() {
        RuleFor(x => x.Name)
            .NotEmpty().WithMessage("Product name required")
            .MaximumLength(200);

        RuleFor(x => x.Price)
            .GreaterThan(0).WithMessage("Price must be positive");

        RuleFor(x => x.StockQuantity)
            .GreaterThanOrEqualTo(0).WithMessage("Stock cannot be negative");
    }
}

// Application/DTOs/ProductResponseDto.cs
public class ProductResponseDto {
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    public int StockQuantity { get; set; }
}

API Controller with Swagger

csharp
// API/Controllers/ProductsController.cs
[ApiController]
[Route("api/products")]
public class ProductsController : ControllerBase {
    private readonly CreateProductHandler _createHandler;
    private readonly IProductRepository _repository;
    private readonly ILogger<ProductsController> _logger;

    public ProductsController(
        CreateProductHandler createHandler,
        IProductRepository repository,
        ILogger<ProductsController> logger) {
        _createHandler = createHandler;
        _repository = repository;
        _logger = logger;
    }

    [HttpPost]
    [ProducesResponseType(typeof(ProductResponseDto), 201)]
    [ProducesResponseType(400)]
    /// <summary>Create a new product</summary>
    public async Task<IActionResult> CreateAsync([FromBody] Product.CreateCommand command) {
        try {
            var productId = await _createHandler.HandleAsync(command);
            var product = await _repository.GetByIdAsync(productId);
            var dto = new ProductResponseDto {
                Id = product.Id,
                Name = product.Name,
                Price = product.Price,
                StockQuantity = product.StockQuantity
            };
            return CreatedAtAction(nameof(GetAsync), new { id = productId }, dto);
        } catch (ValidationException ex) {
            return BadRequest(new { errors = ex.Errors });
        }
    }

    [HttpGet("{id:int}")]
    [ProducesResponseType(typeof(ProductResponseDto), 200)]
    [ProducesResponseType(404)]
    /// <summary>Get product by ID</summary>
    public async Task<IActionResult> GetAsync(int id) {
        var product = await _repository.GetByIdAsync(id);
        if (product == null) {
            _logger.LogWarning("Product not found: {ProductId}", id);
            return NotFound();
        }

        return Ok(new ProductResponseDto {
            Id = product.Id,
            Name = product.Name,
            Price = product.Price,
            StockQuantity = product.StockQuantity
        });
    }
}

Integration Test Example

csharp
// Tests/Integration/ProductsControllerTests.cs
public class ProductsControllerTests : IAsyncLifetime {
    private readonly WebApplicationFactory<Program> _factory;
    private HttpClient _client;

    public ProductsControllerTests() {
        _factory = new WebApplicationFactory<Program>()
            .WithWebHostBuilder(builder => {
                builder.ConfigureServices(services => {
                    var descriptor = services.SingleOrDefault(
                        d => d.ServiceType == typeof(DbContextOptions<ApplicationDbContext>));
                    services.Remove(descriptor);
                    services.AddDbContext<ApplicationDbContext>(
                        options => options.UseInMemoryDatabase("TestDb"));
                });
            });
    }

    public async Task InitializeAsync() {
        _client = _factory.CreateClient();
    }

    public async Task DisposeAsync() {
        _client?.Dispose();
        _factory?.Dispose();
    }

    [Fact]
    public async Task Create_WithValidData_Returns201() {
        var payload = new {
            Name = "Laptop",
            Description = "High-end laptop",
            Price = 999.99,
            StockQuantity = 10
        };
        var content = new StringContent(
            JsonSerializer.Serialize(payload),
            Encoding.UTF8,
            "application/json");

        var response = await _client.PostAsync("/api/products", content);

        Assert.Equal(HttpStatusCode.Created, response.StatusCode);
        Assert.NotNull(response.Headers.Location);
    }

    [Fact]
    public async Task Create_WithInvalidPrice_Returns400() {
        var payload = new {
            Name = "Invalid",
            Price = -10,
            StockQuantity = 5
        };
        var content = new StringContent(
            JsonSerializer.Serialize(payload),
            Encoding.UTF8,
            "application/json");

        var response = await _client.PostAsync("/api/products", content);

        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
    }
}
06

Code Review Preparation

Evaluators will ask these questions. Have concise, confident answers ready.

Architecture & Design Decisions

Q: "Why did you choose this architecture?"

Good Answer: "I used Clean Architecture (Onion Model) because it separates concerns into independent layers. The domain layer contains pure business logic, the application layer orchestrates use cases, and the infrastructure layer handles persistence. This separation makes testing easier—I can test handlers in isolation by mocking repositories—and it makes the codebase more maintainable and scalable."

Q: "How would you scale this system?"

Good Answer: "First, I'd identify the bottleneck. If it's database reads, I'd add caching (Redis) at the repository layer. If it's writes, I'd implement CQRS to separate read and write models. For horizontal scaling, I'd containerize with Docker and orchestrate with Kubernetes. I'd also add logging and monitoring to track performance."

Q: "What would you do differently if building this again?"

Good Answer: "I'd evaluate whether the full 4-layer architecture was necessary—maybe a simpler layered model would suffice. I might also consider event sourcing if the domain involves complex state changes. And I'd invest more upfront in integration tests for critical paths rather than discovering issues in production."

Q: "Tell me about your error handling strategy."

Good Answer: "I define custom exception types (DomainException, ValidationException) to signal specific error conditions. At the API layer, middleware catches exceptions and returns consistent HTTP responses with appropriate status codes. I log all errors with context (RequestId, UserId) for debugging. Clients receive user-friendly messages, never stack traces."

Testing & Quality

Q: "Explain your testing strategy."

Good Answer: "I follow the testing pyramid: mostly unit tests for business logic (handlers, services), fewer integration tests for API endpoints, and a few end-to-end tests for critical user workflows. Unit tests use Moq to isolate dependencies. Integration tests use WebApplicationFactory with an in-memory database. I aim for >70% coverage on critical paths."

Q: "How do you ensure database migrations don't break production?"

Good Answer: "I design migrations to be reversible and backward-compatible. I avoid deleting columns immediately—instead, I deprecate them first. I test migrations locally and in a staging environment before production. I also keep database schema changes separate from code changes when possible."

Q: "What's your approach to validation?"

Good Answer: "I use FluentValidation to validate input at the application layer boundary. Validators check format, length, and business rules (e.g., email uniqueness). If validation fails, handlers throw ValidationException, which middleware converts to a 400 response with detailed error messages. This keeps validation logic testable and separate from controllers."

Code Quality

Q: "Walk me through your dependency injection setup."

Good Answer: "In Program.cs, I register all dependencies with the IServiceCollection. Repositories are registered as Scoped (one per HTTP request). Handlers and services are Scoped as well. External dependencies (password hashers, email services) are abstracted behind interfaces so they can be swapped for testing. This makes the container a single source of truth for wiring."

Q: "How do you prevent common bugs like SQL injection?"

Good Answer: "I use Entity Framework Core with parameterized queries—never string concatenation. EF translates LINQ queries to parameterized SQL. I also validate all user input before passing to the database layer. Additionally, I run static analysis tools like SonarAnalyzer to catch security issues early."

Communication & Trade-Offs

Q: "What trade-offs did you make?"

Good Answer: "I chose simplicity over premature optimization. For example, I used synchronous validation instead of async validators because the overhead wasn't justified. I also decided to use in-memory caching initially rather than Redis because the system's traffic was predictable. These decisions are easy to revisit as requirements change."

Q: "What would you do if you had more time?"

Good Answer: "I'd add distributed tracing (Application Insights or Jaeger) to track requests across services if we scale horizontally. I'd also implement API versioning to maintain backward compatibility. And I'd spend more time on performance testing—profiling the most common queries and ensuring they use indexes efficiently."

07

Common Mistakes to Avoid

Evaluators see these patterns frequently. Avoid them to stand out.

❌ No Tests

Problem: Submitting code with zero unit or integration tests signals that you haven't validated your code. Tests are non-negotiable in production systems.

Fix: Write at minimum 10-15 unit tests covering the happy path and edge cases. Add 3-5 integration tests for critical endpoints.

❌ God Classes

Problem: A single UserService with 50+ methods handling users, validation, emails, and logging. Violates Single Responsibility.

Fix: Break into smaller classes: UserHandler, PasswordValidator, EmailService. Each has one reason to change.

❌ SQL Injection (String Concatenation)

csharp
// ❌ WRONG - SQL Injection vulnerability
var query = $"SELECT * FROM Users WHERE Email = '{email}'";
var user = dbContext.Users.FromSqlInterpolated(query);

// ✅ CORRECT - Parameterized query
var user = await dbContext.Users.FirstOrDefaultAsync(u => u.Email == email);

❌ Synchronous I/O

Problem: Using .Result or .Wait() on async calls blocks threads and causes deadlocks under load.

csharp
// ❌ WRONG - Blocks thread
var user = _repository.GetByIdAsync(id).Result;

// ✅ CORRECT - Async all the way
var user = await _repository.GetByIdAsync(id);

❌ No Error Handling

Problem: Exceptions bubble up unhandled, returning 500 with stack traces. Clients get no useful information.

Fix: Add ErrorHandlingMiddleware to catch exceptions and return appropriate status codes with meaningful messages.

❌ No Logging

Problem: When something breaks in production, there's no audit trail. You can't debug issues.

Fix: Use ILogger throughout. Log at entry/exit of important methods, and log all errors with context.

❌ Tight Coupling

csharp
// ❌ WRONG - Tightly coupled
public class UserHandler {
    private EfUserRepository _repository = new EfUserRepository();
}

// ✅ CORRECT - Loosely coupled via interface
public class UserHandler {
    private readonly IUserRepository _repository;
    public UserHandler(IUserRepository repository) => _repository = repository;
}

❌ Unvalidated User Input

Problem: Accepting any input (negative prices, huge strings) and trusting the database to reject it.

Fix: Validate at the application layer using FluentValidation. Fail fast with clear error messages.

❌ Inconsistent HTTP Status Codes

Problem: Always returning 200 or 500, even when errors occur. Clients can't tell if a request succeeded.

Fix: Use correct status codes: 201 for Created, 400 for Bad Request, 404 for Not Found, 409 for Conflict, 500 for Internal Server Error.

❌ Magic Numbers & Strings

csharp
// ❌ WRONG - Magic number
if (password.Length < 8) throw new Exception("Too short");

// ✅ CORRECT - Named constant
const int MinPasswordLength = 8;
if (password.Length < MinPasswordLength) {
    throw new ValidationException("Password too short");
}

❌ Missing Documentation

Problem: No README, no Swagger docs, no commit messages. Evaluators can't understand the project architecture.

Fix: Include a README with setup instructions, enable Swagger UI, write meaningful commit messages, add XML comments to public methods.

❌ Incomplete Git History

Problem: Entire project committed in one massive commit, or commits like "fix", "update", "oops". No way to track design decisions.

Fix: Commit frequently with descriptive messages: "Add user repository with GetByIdAsync and CreateAsync methods".

08

Pre-Submission Checklist

Use this checklist the night before submission. Don't submit until all items are green.

Architecture & Code

Testing

Error Handling & Logging

Validation

API & Documentation

Git & Documentation

Final Tests

✓
You're ready to submit! If you've checked every item above, your code is at professional quality. The evaluator will be impressed by your architecture decisions and thoroughness. Good luck!