Clean Code Architecture C#: Panduan Lengkap dan Pertanyaan Interview 2026

Pelajari Clean Code Architecture di C# dengan prinsip SOLID, pola Repository, Dependency Injection, dan pertanyaan interview yang sering muncul untuk developer .NET.

Clean Code Architecture C# diagram showing layers and dependencies

Clean Code Architecture di C# menggabungkan prinsip Clean Code dari Robert C. Martin dengan pola Clean Architecture untuk menghasilkan aplikasi .NET yang mudah dipelihara, dapat diuji, dan skalabel. Interview teknis semakin banyak membahas konsep-konsep ini karena menunjukkan cara seorang kandidat berpikir tentang desain perangkat lunak di luar sekadar membuat kode berfungsi.

Insight Interview

Ketika ditanya tentang Clean Architecture, interviewer mengharapkan kandidat menjelaskan aturan dependensi: dependensi source code mengarah ke dalam, menuju kebijakan tingkat lebih tinggi. Layer Domain tidak mengetahui apa pun tentang Infrastructure, bukan sebaliknya.

Prinsip SOLID sebagai Fondasi Clean Code di C#

Prinsip SOLID membentuk tulang punggung Clean Code di C#. Dokumentasi resmi Microsoft tentang .NET fundamentals merekomendasikan pola-pola ini untuk aplikasi enterprise. Setiap prinsip menangani masalah pemeliharaan yang spesifik.

Single Responsibility Principle (SRP): Sebuah class hanya memiliki satu alasan untuk berubah. OrderService di bawah hanya menangani pemrosesan pesanan, mendelegasikan persistensi dan notifikasi ke komponen terpisah.

OrderService.cscsharp
public class OrderService
{
    private readonly IOrderRepository _orderRepository;
    private readonly INotificationService _notificationService;

    public OrderService(
        IOrderRepository orderRepository,
        INotificationService notificationService)
    {
        _orderRepository = orderRepository;
        _notificationService = notificationService;
    }

    public async Task<Order> CreateOrderAsync(CreateOrderRequest request)
    {
        // Validates and creates the domain entity
        var order = Order.Create(request.CustomerId, request.Items);
        
        // Persists through the repository abstraction
        await _orderRepository.AddAsync(order);
        
        // Notifies through a separate service
        await _notificationService.SendOrderConfirmationAsync(order);
        
        return order;
    }
}

Open/Closed Principle (OCP): Class tetap terbuka untuk ekstensi tetapi tertutup untuk modifikasi. Metode pembayaran baru memerlukan class baru, bukan perubahan pada class yang sudah ada.

IPaymentProcessor.cscsharp
public interface IPaymentProcessor
{
    string PaymentMethod { get; }
    Task<PaymentResult> ProcessAsync(Payment payment);
}

// StripePaymentProcessor.cs
public class StripePaymentProcessor : IPaymentProcessor
{
    public string PaymentMethod => "Stripe";
    
    public async Task<PaymentResult> ProcessAsync(Payment payment)
    {
        // Stripe-specific implementation
        var charge = await _stripeClient.CreateChargeAsync(payment.Amount);
        return new PaymentResult(charge.Id, charge.Status == "succeeded");
    }
}

// PaymentService.cs
public class PaymentService
{
    private readonly IEnumerable<IPaymentProcessor> _processors;

    public PaymentService(IEnumerable<IPaymentProcessor> processors)
    {
        _processors = processors;
    }

    public async Task<PaymentResult> ProcessPaymentAsync(
        Payment payment, string method)
    {
        var processor = _processors
            .FirstOrDefault(p => p.PaymentMethod == method)
            ?? throw new NotSupportedException($"Payment method {method} not supported");
        
        return await processor.ProcessAsync(payment);
    }
}

Menambahkan dukungan PayPal berarti menambahkan class PayPalPaymentProcessor. PaymentService tetap tidak berubah.

Empat Layer Clean Architecture di .NET

Clean Architecture mengorganisasi kode menjadi layer konsentris. Repository Clean Architecture oleh Jason Taylor menyediakan template .NET yang banyak diadopsi. Setiap layer memiliki tanggung jawab eksplisit dan dependensi mengalir ke dalam.

LayerTanggung JawabDependensi
DomainEntity, value object, domain eventTidak ada
ApplicationUse case, DTO, interfaceDomain
InfrastructureDatabase, API eksternal, file systemApplication, Domain
PresentationController, view, API endpointApplication
Domain/Entities/Customer.cscsharp
public class Customer
{
    public Guid Id { get; private set; }
    public string Email { get; private set; }
    public CustomerStatus Status { get; private set; }

    private Customer() { } // EF Core constructor

    public static Customer Create(string email)
    {
        if (string.IsNullOrWhiteSpace(email))
            throw new DomainException("Email cannot be empty");
        
        return new Customer
        {
            Id = Guid.NewGuid(),
            Email = email.ToLowerInvariant(),
            Status = CustomerStatus.Active
        };
    }

    public void Deactivate()
    {
        if (Status == CustomerStatus.Inactive)
            throw new DomainException("Customer already inactive");
        
        Status = CustomerStatus.Inactive;
    }
}

Entity Domain mengenkapsulasi aturan bisnis. Entity ini memvalidasi invariannya sendiri dan mengekspos behavior melalui method, bukan setter.

Mengapa Private Setter?

Private setter mencegah kode eksternal menempatkan entity dalam state yang tidak valid. Method Deactivate() menegakkan aturan bahwa customer yang sudah tidak aktif tidak dapat dinonaktifkan lagi. Pola ini sering muncul dalam implementasi Domain-Driven Design dan Clean Architecture.

Pola Dependency Injection untuk Clean Architecture

Dependency Injection (DI) memungkinkan inversi dependensi yang dibutuhkan Clean Architecture. .NET 10 menyertakan container DI bawaan yang mendukung constructor injection, scoped lifetime, dan keyed service yang diperkenalkan di .NET 8.

Program.cs (Minimal API)csharp
var builder = WebApplication.CreateBuilder(args);

// Domain services - typically transient
builder.Services.AddTransient<IOrderValidator, OrderValidator>();

// Application services - scoped per request
builder.Services.AddScoped<IOrderService, OrderService>();

// Infrastructure - scoped to share DbContext
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();

// External clients - singleton with HttpClient pooling
builder.Services.AddHttpClient<IPaymentGateway, StripeGateway>(client =>
{
    client.BaseAddress = new Uri("https://api.stripe.com/v1/");
});

// Keyed services for multiple implementations (.NET 8+)
builder.Services.AddKeyedScoped<INotificationService, EmailNotificationService>("email");
builder.Services.AddKeyedScoped<INotificationService, SmsNotificationService>("sms");

var app = builder.Build();

Layer Application mendefinisikan interface. Layer Infrastructure mengimplementasikannya. Layer Presentation (atau composition root) menghubungkan semuanya.

Pola Repository dengan Entity Framework Core 9

Pola Repository mengabstraksi akses data di balik interface yang berpusat pada domain. EF Core 9, yang dirilis bersama .NET 10, menyediakan ORM yang mendasarinya.

Application/Interfaces/IOrderRepository.cscsharp
public interface IOrderRepository
{
    Task<Order?> GetByIdAsync(Guid id, CancellationToken ct = default);
    Task<IReadOnlyList<Order>> GetByCustomerAsync(Guid customerId, CancellationToken ct = default);
    Task AddAsync(Order order, CancellationToken ct = default);
    void Update(Order order);
}

// Infrastructure/Repositories/OrderRepository.cs
public class OrderRepository : IOrderRepository
{
    private readonly ApplicationDbContext _context;

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

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

    public async Task<IReadOnlyList<Order>> GetByCustomerAsync(
        Guid customerId, CancellationToken ct = default)
    {
        return await _context.Orders
            .Where(o => o.CustomerId == customerId)
            .OrderByDescending(o => o.CreatedAt)
            .ToListAsync(ct);
    }

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

    public void Update(Order order)
    {
        _context.Orders.Update(order);
    }
}

Repository mengembalikan entity domain, bukan DTO. Mapping ke DTO terjadi di layer Application, menjaga Domain tetap murni.

Siap menguasai wawancara .NET Anda?

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

Unit Testing Komponen Clean Architecture

Clean Architecture membuat pengujian menjadi mudah karena dependensi diinjeksikan melalui interface. xUnit dan NSubstitute menyediakan framework pengujian dan library mocking untuk .NET.

OrderServiceTests.cscsharp
public class OrderServiceTests
{
    private readonly IOrderRepository _orderRepository;
    private readonly INotificationService _notificationService;
    private readonly OrderService _sut;

    public OrderServiceTests()
    {
        _orderRepository = Substitute.For<IOrderRepository>();
        _notificationService = Substitute.For<INotificationService>();
        _sut = new OrderService(_orderRepository, _notificationService);
    }

    [Fact]
    public async Task CreateOrderAsync_ValidRequest_PersistsAndNotifies()
    {
        // Arrange
        var request = new CreateOrderRequest(
            CustomerId: Guid.NewGuid(),
            Items: new[] { new OrderItemDto("SKU-001", 2, 29.99m) });

        // Act
        var order = await _sut.CreateOrderAsync(request);

        // Assert
        await _orderRepository.Received(1).AddAsync(Arg.Any<Order>());
        await _notificationService.Received(1)
            .SendOrderConfirmationAsync(Arg.Is<Order>(o => o.Id == order.Id));
    }

    [Fact]
    public async Task CreateOrderAsync_EmptyItems_ThrowsDomainException()
    {
        // Arrange
        var request = new CreateOrderRequest(
            CustomerId: Guid.NewGuid(),
            Items: Array.Empty<OrderItemDto>());

        // Act & Assert
        await Assert.ThrowsAsync<DomainException>(
            () => _sut.CreateOrderAsync(request));
    }
}

Test memverifikasi behavior, bukan implementasi. _sut (system under test) berinteraksi dengan mock. Assertion memeriksa bahwa method yang benar dipanggil dengan argumen yang benar.

Pertanyaan Interview tentang Clean Architecture C#

Interview teknis menguji pemahaman Clean Architecture di berbagai level. Pertanyaan-pertanyaan ini muncul dalam interview .NET senior.

Jebakan Interview Umum

Kandidat sering membingungkan Clean Architecture dengan arsitektur N-tier. Perbedaan utamanya: dalam Clean Architecture, dependensi mengarah ke dalam menuju Domain. Dalam N-tier, setiap layer bergantung pada layer di bawahnya, membuat Domain bergantung pada Infrastructure.

T: Bagaimana Clean Architecture berbeda dari arsitektur berlapis tradisional?

Arsitektur berlapis tradisional membuat setiap layer bergantung pada layer di bawahnya: Presentation bergantung pada Business Logic, yang bergantung pada Data Access. Clean Architecture membalik ini: Domain tidak memiliki dependensi, Application bergantung pada Domain, dan Infrastructure bergantung pada keduanya. Inversi ini berarti teknologi database dapat berubah tanpa menyentuh aturan bisnis.

T: Kapan sebaiknya tidak menggunakan Clean Architecture?

Clean Architecture menambah indirection. Untuk aplikasi yang banyak CRUD tanpa aturan bisnis yang kompleks, overhead melebihi manfaatnya. API sederhana yang memproksi tabel database secara langsung tidak memerlukan empat layer. Nilai muncul ketika kompleksitas logika bisnis membenarkan pemisahan tersebut.

T: Bagaimana cara menangani cross-cutting concern seperti logging dan caching?

Dua pola bekerja dengan baik: pola decorator dan middleware. Caching decorator membungkus interface repository, mengimplementasikan interface yang sama sambil menambahkan logika cache. Logging biasanya menggunakan middleware atau DI interceptor yang membungkus panggilan service tanpa mencemari logika bisnis.

CachingOrderRepository.cs (Decorator pattern)csharp
public class CachingOrderRepository : IOrderRepository
{
    private readonly IOrderRepository _inner;
    private readonly IDistributedCache _cache;

    public CachingOrderRepository(
        IOrderRepository inner,
        IDistributedCache cache)
    {
        _inner = inner;
        _cache = cache;
    }

    public async Task<Order?> GetByIdAsync(Guid id, CancellationToken ct = default)
    {
        var cacheKey = $"order:{id}";
        var cached = await _cache.GetStringAsync(cacheKey, ct);
        
        if (cached is not null)
            return JsonSerializer.Deserialize<Order>(cached);
        
        var order = await _inner.GetByIdAsync(id, ct);
        
        if (order is not null)
        {
            await _cache.SetStringAsync(
                cacheKey,
                JsonSerializer.Serialize(order),
                new DistributedCacheEntryOptions
                {
                    AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5)
                },
                ct);
        }
        
        return order;
    }

    // Other methods delegate to _inner
}

T: Bagaimana struktur validasi dalam Clean Architecture?

Validasi terjadi di dua level. Validasi domain (invariant) berada di entity: Order tidak dapat memiliki nol item. Validasi application (input validation) berada di command handler atau validator: request harus menyertakan ID customer yang valid. FluentValidation terintegrasi dengan baik untuk input validation, sementara validasi domain tetap di constructor dan method entity.

Organisasi Kode dan Konvensi Penamaan

Struktur proyek mengkomunikasikan arsitektur. Template Clean Architecture standar mengorganisasi proyek berdasarkan layer.

text
src/
ā”œā”€ā”€ MyApp.Domain/
│   ā”œā”€ā”€ Entities/
│   ā”œā”€ā”€ ValueObjects/
│   ā”œā”€ā”€ Events/
│   └── Exceptions/
ā”œā”€ā”€ MyApp.Application/
│   ā”œā”€ā”€ Common/
│   │   ā”œā”€ā”€ Interfaces/
│   │   └── Behaviors/
│   ā”œā”€ā”€ Orders/
│   │   ā”œā”€ā”€ Commands/
│   │   ā”œā”€ā”€ Queries/
│   │   └── EventHandlers/
│   └── Customers/
ā”œā”€ā”€ MyApp.Infrastructure/
│   ā”œā”€ā”€ Persistence/
│   ā”œā”€ā”€ Services/
│   └── Configuration/
└── MyApp.WebApi/
    ā”œā”€ā”€ Controllers/
    ā”œā”€ā”€ Filters/
    └── Middleware/

Vertical slice (Orders, Customers) mengelompokkan use case yang terkait. Organisasi ini lebih baik skalanya dibanding horizontal slice (Commands, Queries) seiring pertumbuhan aplikasi.

Pertimbangan Performa dalam Clean Architecture

Abstraksi memiliki biaya. Setiap pemanggilan interface menambah indirection. Praktik-praktik ini meminimalkan overhead sambil mempertahankan testability.

Gunakan record untuk DTO: Record menghasilkan implementasi Equals dan GetHashCode yang efisien. Record bersifat immutable secara default, mencegah mutasi yang tidak disengaja.

Application/Orders/Queries/OrderDto.cscsharp
public record OrderDto(
    Guid Id,
    Guid CustomerId,
    IReadOnlyList<OrderItemDto> Items,
    decimal Total,
    DateTime CreatedAt);

public record OrderItemDto(
    string Sku,
    int Quantity,
    decimal UnitPrice);

Hindari over-abstraction: Tidak setiap class memerlukan interface. Abstraksi dependensi eksternal (database, HTTP client, file system). Service domain internal yang hanya memiliki satu implementasi jarang memerlukan interface.

Profil sebelum mengoptimasi: Overhead layer Clean Architecture biasanya dapat diabaikan dibandingkan operasi I/O. Query database yang memakan 50ms jauh melebihi mikrodetik yang dihabiskan dalam method dispatch.

Menerapkan Prinsip Clean Code pada Method C#

Clean Code berfokus pada keterbacaan di level method dan class. Praktik-praktik ini berlaku terlepas dari pola arsitektur.

Method melakukan satu hal: Method bernama ProcessOrderAndSendEmail melanggar SRP. Pisahkan menjadi ProcessOrder dan SendOrderConfirmation.

Nama yang bermakna: CalculateOrderTotal mengkomunikasikan tujuan. DoCalculation tidak. Nama variabel mengikuti aturan yang sama: customerOrders lebih baik dari list.

Method kecil: Jika method melebihi 20 baris, kemungkinan melakukan terlalu banyak. Ekstrak helper method dengan nama deskriptif.

csharp
// Before: Long method doing multiple things
public decimal CalculateInvoiceTotal(Invoice invoice)
{
    decimal subtotal = 0;
    foreach (var item in invoice.Items)
    {
        subtotal += item.Quantity * item.UnitPrice;
    }
    
    decimal discount = 0;
    if (invoice.Customer.IsPreferred)
    {
        discount = subtotal * 0.1m;
    }
    
    decimal tax = (subtotal - discount) * 0.2m;
    
    return subtotal - discount + tax;
}

// After: Small methods with single responsibilities
public decimal CalculateInvoiceTotal(Invoice invoice)
{
    var subtotal = CalculateSubtotal(invoice.Items);
    var discount = CalculateDiscount(subtotal, invoice.Customer);
    var tax = CalculateTax(subtotal - discount);
    
    return subtotal - discount + tax;
}

private decimal CalculateSubtotal(IEnumerable<InvoiceItem> items)
    => items.Sum(item => item.Quantity * item.UnitPrice);

private decimal CalculateDiscount(decimal subtotal, Customer customer)
    => customer.IsPreferred ? subtotal * 0.1m : 0;

private decimal CalculateTax(decimal taxableAmount)
    => taxableAmount * 0.2m;

Versi yang di-refactor terbaca seperti ringkasan logika bisnis. Setiap helper method dapat diuji secara independen.

Mulai berlatih!

Uji pengetahuan Anda dengan simulator wawancara dan tes teknis kami.

Poin Penting Clean Code Architecture di C#

  • Aturan dependensi: dependensi kode mengarah ke dalam, dari Infrastructure menuju Domain, tidak pernah ke luar
  • Prinsip SOLID memandu desain class: single responsibility, open for extension, Liskov substitution, interface segregation, dependency inversion
  • Empat layer memisahkan concern: Domain (entity), Application (use case), Infrastructure (sistem eksternal), Presentation (API/UI)
  • Dependency injection menghubungkan layer di composition root, biasanya di Program.cs
  • Pola repository mengabstraksi akses data di balik interface yang berpusat pada domain yang mengembalikan entity, bukan DTO
  • Unit test memverifikasi behavior dengan mocking interface, memvalidasi bahwa method yang benar menerima argumen yang benar
  • Clean Code di level method: method kecil, nama bermakna, single responsibility
  • Biaya performa abstraksi biasanya dapat diabaikan dibandingkan operasi I/O; profil sebelum mengoptimasi
  • Pertanyaan interview menguji pemahaman aturan dependensi, trade-off, dan pola praktis seperti decorator untuk cross-cutting concern
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 24 Agustus 2026

Tag

#clean-architecture
#csharp
#solid
#dotnet
#best-practices

Bagikan

Artikel terkait