Clean Architecture .NET 2026: CQRS, MediatR, dan Pertanyaan Interview Developer

Panduan lengkap Clean Architecture di .NET dengan CQRS dan MediatR 14. Pelajari pola arsitektur berlapis, pipeline behaviors, dan persiapan interview developer senior.

Clean Architecture .NET dengan CQRS dan MediatR

Clean Architecture di .NET memisahkan logika bisnis dari infrastruktur, membuat aplikasi lebih mudah diuji, dipelihara, dan di-scale. Dikombinasikan dengan CQRS (Command Query Responsibility Segregation) dan MediatR, pola arsitektur ini memberikan pendekatan terstruktur untuk membangun aplikasi enterprise yang sering dinilai dalam interview teknis.

Penting untuk Interview

Pertanyaan Clean Architecture muncul di 70% interview senior .NET. Interviewer mengharapkan kandidat menjelaskan dependency rule: dependency mengarah ke dalam, dengan domain layer tidak memiliki dependency pada framework eksternal.

Layer Clean Architecture dan Dependency Rule

Template Clean Architecture dari Ardalis untuk ASP.NET Core 10 mengorganisir kode ke dalam empat layer konsentris. Setiap layer memiliki tanggung jawab spesifik dan aturan dependency yang ketat.

csharp
// Core/Domain Layer - Tanpa dependency eksternal
// src/Core/Domain/Entities/Order.cs
namespace Core.Domain.Entities;

public class Order
{
    public Guid Id { get; private set; }
    public string CustomerEmail { get; private set; }
    public List<OrderLine> Lines { get; private set; } = new();
    public OrderStatus Status { get; private set; }
    public DateTime CreatedAt { get; private set; }

    // Logika domain dienkapsulasi dalam entity
    public void AddLine(Product product, int quantity)
    {
        if (Status != OrderStatus.Draft)
            throw new InvalidOperationException("Cannot modify a submitted order");

        var existingLine = Lines.FirstOrDefault(l => l.ProductId == product.Id);
        if (existingLine is not null)
        {
            existingLine.IncreaseQuantity(quantity);
            return;
        }

        Lines.Add(new OrderLine(product.Id, product.Price, quantity));
    }

    public void Submit()
    {
        if (!Lines.Any())
            throw new InvalidOperationException("Cannot submit an empty order");

        Status = OrderStatus.Submitted;
    }
}

Domain layer berisi entities, value objects, dan domain services. Layer ini tidak mengetahui apapun tentang database, HTTP, atau framework eksternal.

Pola CQRS: Memisahkan Read dari Write

CQRS membagi operasi menjadi Commands (operasi write yang mengubah state) dan Queries (operasi read yang mengembalikan data). Pemisahan ini memungkinkan scaling dan optimisasi independen untuk setiap jalur.

csharp
// UseCases Layer - Commands dan Queries
// src/UseCases/Orders/Commands/CreateOrder/CreateOrderCommand.cs
namespace UseCases.Orders.Commands.CreateOrder;

public record CreateOrderCommand(
    string CustomerEmail,
    List<OrderLineDto> Lines
) : IRequest<Result<Guid>>;

public record OrderLineDto(Guid ProductId, int Quantity);

Commands mengembalikan data minimal, biasanya hanya indikator sukses atau ID yang baru dibuat. Queries mengembalikan DTO yang dioptimalkan untuk konsumer.

src/UseCases/Orders/Queries/GetOrderById/GetOrderByIdQuery.cscsharp
namespace UseCases.Orders.Queries.GetOrderById;

public record GetOrderByIdQuery(Guid OrderId) : IRequest<Result<OrderDetailsDto>>;

public record OrderDetailsDto(
    Guid Id,
    string CustomerEmail,
    string Status,
    decimal TotalAmount,
    List<OrderLineDetailsDto> Lines
);

MediatR 14: Pipeline Behaviors dan Implementasi Handler

MediatR 14.2 menyediakan infrastruktur messaging untuk CQRS. Setiap command atau query memiliki tepat satu handler, menegakkan single responsibility.

src/UseCases/Orders/Commands/CreateOrder/CreateOrderHandler.cscsharp
namespace UseCases.Orders.Commands.CreateOrder;

public class CreateOrderHandler : IRequestHandler<CreateOrderCommand, Result<Guid>>
{
    private readonly IOrderRepository _orderRepository;
    private readonly IProductRepository _productRepository;
    private readonly IUnitOfWork _unitOfWork;

    public CreateOrderHandler(
        IOrderRepository orderRepository,
        IProductRepository productRepository,
        IUnitOfWork unitOfWork)
    {
        _orderRepository = orderRepository;
        _productRepository = productRepository;
        _unitOfWork = unitOfWork;
    }

    public async Task<Result<Guid>> Handle(
        CreateOrderCommand request,
        CancellationToken cancellationToken)
    {
        // Load products untuk validasi dan mendapatkan harga
        var productIds = request.Lines.Select(l => l.ProductId).ToList();
        var products = await _productRepository
            .GetByIdsAsync(productIds, cancellationToken);

        if (products.Count != productIds.Count)
            return Result.NotFound("One or more products not found");

        // Membuat order menggunakan logika domain
        var order = new Order(request.CustomerEmail);

        foreach (var line in request.Lines)
        {
            var product = products.First(p => p.Id == line.ProductId);
            order.AddLine(product, line.Quantity);
        }

        await _orderRepository.AddAsync(order, cancellationToken);
        await _unitOfWork.SaveChangesAsync(cancellationToken);

        return Result.Success(order.Id);
    }
}

Siap menguasai wawancara .NET Anda?

Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.

Pipeline Behaviors untuk Cross-Cutting Concerns

Pipeline behaviors di MediatR menangani logging, validasi, caching, dan manajemen transaksi tanpa mengotori handlers.

src/UseCases/Common/Behaviors/ValidationBehavior.cscsharp
namespace UseCases.Common.Behaviors;

public class ValidationBehavior<TRequest, TResponse>
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    private readonly IEnumerable<IValidator<TRequest>> _validators;

    public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
    {
        _validators = validators;
    }

    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken)
    {
        if (!_validators.Any())
            return await next();

        var context = new ValidationContext<TRequest>(request);

        var validationResults = await Task.WhenAll(
            _validators.Select(v => v.ValidateAsync(context, cancellationToken)));

        var failures = validationResults
            .SelectMany(r => r.Errors)
            .Where(f => f is not null)
            .ToList();

        if (failures.Any())
            throw new ValidationException(failures);

        return await next();
    }
}

Daftarkan behaviors sesuai urutan eksekusi. Validasi biasanya berjalan pertama, diikuti logging, kemudian handling transaksi.

src/Web/Program.cscsharp
builder.Services.AddMediatR(cfg =>
{
    cfg.RegisterServicesFromAssembly(typeof(CreateOrderCommand).Assembly);
    cfg.AddOpenBehavior(typeof(ValidationBehavior<,>));
    cfg.AddOpenBehavior(typeof(LoggingBehavior<,>));
    cfg.AddOpenBehavior(typeof(TransactionBehavior<,>));
});

Infrastructure Layer: Implementasi Repository

Infrastructure layer mengimplementasikan interface yang didefinisikan di core layer. Repository Entity Framework Core menerjemahkan operasi domain ke panggilan database.

src/Infrastructure/Data/Repositories/OrderRepository.cscsharp
namespace Infrastructure.Data.Repositories;

public class OrderRepository : IOrderRepository
{
    private readonly AppDbContext _context;

    public OrderRepository(AppDbContext context)
    {
        _context = context;
    }

    public async Task<Order?> GetByIdAsync(
        Guid id,
        CancellationToken cancellationToken = default)
    {
        return await _context.Orders
            .Include(o => o.Lines)
            .FirstOrDefaultAsync(o => o.Id == id, cancellationToken);
    }

    public async Task AddAsync(
        Order order,
        CancellationToken cancellationToken = default)
    {
        await _context.Orders.AddAsync(order, cancellationToken);
    }

    public async Task<IReadOnlyList<Order>> GetByCustomerEmailAsync(
        string email,
        CancellationToken cancellationToken = default)
    {
        return await _context.Orders
            .Where(o => o.CustomerEmail == email)
            .OrderByDescending(o => o.CreatedAt)
            .ToListAsync(cancellationToken);
    }
}

Pertanyaan Interview: Apa yang Harus Diketahui Developer Senior

Interview teknis untuk posisi senior .NET sering mencakup pertanyaan Clean Architecture. Berikut pola-pola yang dinilai interviewer:

Q: Mengapa domain layer tidak memiliki dependency pada framework eksternal?

Domain layer berisi aturan bisnis yang harus tetap stabil terlepas dari perubahan infrastruktur. Jika domain bergantung pada Entity Framework, beralih ke Dapper atau database berbeda memerlukan perubahan logika bisnis. Dependency rule memastikan infrastruktur adalah detail yang dapat ditukar tanpa mempengaruhi behavior inti.

Q: Kapan CQRS berlebihan?

CQRS menambah kompleksitas yang tidak diperlukan aplikasi CRUD sederhana. Jika model read dan write hampir identik, jika tidak ada kebutuhan scaling terpisah, dan jika tim kecil, arsitektur yang lebih sederhana lebih tepat. CQRS bersinar ketika model read sangat berbeda dari model write, ketika event sourcing diperlukan, atau ketika beban read dan write perlu scaling independen.

Q: Bagaimana menangani transaksi lintas multiple aggregates?

csharp
// Transaction behavior membungkus seluruh eksekusi handler
public class TransactionBehavior<TRequest, TResponse>
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    private readonly IUnitOfWork _unitOfWork;

    public TransactionBehavior(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
    }

    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken)
    {
        // Commands yang memodifikasi multiple aggregates menggunakan transaksi eksplisit
        if (request is not ICommand)
            return await next();

        await using var transaction = await _unitOfWork
            .BeginTransactionAsync(cancellationToken);

        try
        {
            var response = await next();
            await transaction.CommitAsync(cancellationToken);
            return response;
        }
        catch
        {
            await transaction.RollbackAsync(cancellationToken);
            throw;
        }
    }
}

Untuk operasi yang mencakup multiple aggregates, gunakan eventual consistency dengan domain events bila memungkinkan. Transaksi sinkron lintas aggregates mengindikasikan potensi masalah desain: aggregates tersebut mungkin seharusnya digabungkan.

Optimisasi Read-Side dengan Model Query Terpisah

Queries melewati domain layer sepenuhnya ketika performa penting. Akses database langsung dengan Dapper atau raw SQL menghindari overhead materialisasi entity graph penuh.

src/Infrastructure/Queries/GetOrderByIdQueryHandler.cscsharp
namespace Infrastructure.Queries;

public class GetOrderByIdQueryHandler
    : IRequestHandler<GetOrderByIdQuery, Result<OrderDetailsDto>>
{
    private readonly IDbConnection _connection;

    public GetOrderByIdQueryHandler(IDbConnection connection)
    {
        _connection = connection;
    }

    public async Task<Result<OrderDetailsDto>> Handle(
        GetOrderByIdQuery request,
        CancellationToken cancellationToken)
    {
        const string sql = """
            SELECT o.Id, o.CustomerEmail, o.Status, o.CreatedAt,
                   SUM(ol.UnitPrice * ol.Quantity) AS TotalAmount
            FROM Orders o
            LEFT JOIN OrderLines ol ON ol.OrderId = o.Id
            WHERE o.Id = @OrderId
            GROUP BY o.Id, o.CustomerEmail, o.Status, o.CreatedAt
            """;

        var order = await _connection.QueryFirstOrDefaultAsync<OrderDetailsDto>(
            new CommandDefinition(sql, new { request.OrderId }, cancellationToken: cancellationToken));

        return order is null
            ? Result.NotFound($"Order {request.OrderId} not found")
            : Result.Success(order);
    }
}

Mulai berlatih!

Uji pengetahuan Anda dengan simulator wawancara dan tes teknis kami.

Persiapan Interview Clean Architecture: Poin-Poin Utama

  • Dependency rule menyatakan bahwa dependency source code hanya boleh mengarah ke dalam. Domain layer tidak mengetahui database, framework, atau mekanisme delivery.
  • CQRS memisahkan commands (perubahan state) dari queries (pengambilan data). Commands melewati domain model dan validasi. Queries dapat melewati domain entities untuk performa.
  • MediatR 14 merutekan requests ke handlers. Pipeline behaviors menangani cross-cutting concerns seperti validasi, logging, dan transaksi secara composable.
  • Interface repository berada di core layer. Implementasi berada di infrastructure. Inversi ini memungkinkan penukaran EF Core dengan Dapper tanpa menyentuh logika bisnis.
  • Jawaban interview harus mencakup trade-offs. Clean Architecture menambah kompleksitas awal tetapi memberikan hasil di aplikasi besar, berumur panjang dengan multiple developers.
  • Modul ASP.NET Core Clean Architecture di SharpSkill mencakup pola tambahan termasuk Specification, Result objects, dan Domain Events.
Tantangan harian

Bisakah kamu menemukan bug di .NET?

Satu potongan kode nyata, satu bug tersembunyi, satu percobaan per hari. Tanpa akun untuk mencoba.

Anthony Fillion-Maillet

Ditulis oleh

Anthony Fillion-Maillet

Pendiri SharpSkill

Developer fullstack selama lebih dari 10 tahun. Ia menjalankan SharpSkill dan bertanggung jawab atas semua yang diterbitkan di sini.

Diperbarui 15 September 2026

Tag

#clean-architecture
#cqrs
#mediatr
#dotnet
#interview

Bagikan

Artikel terkait