Writing Clean Code with C# — Readability & Simplicity
Practical clean code techniques for C# developers: meaningful naming, small focused methods, avoiding magic numbers, guard clauses, immutability, and reducing complexity.
Writing Clean Code with C# — Readability & Simplicity
Clean code is code that communicates intent clearly, has no surprises, and is easy to change. It is a craft: deliberate and practiced, not a one-time event.
Meaningful Names
// BAD — abbreviations and generic names
int d = 7;
List<string> lst = new();
void Proc(Customer c) { }
// GOOD — names reveal intent
int daysUntilExpiry = 7;
List<string> failedValidationMessages = new();
void SendWelcomeEmail(Customer newCustomer) { }
// BAD — misleading names
bool isNotValid = !customer.IsValid();
if (!isNotValid) { } // double negative, confusing
// GOOD
bool isValid = customer.IsValid();
if (isValid) { }
// BAD — generic type names
public class Data { }
public class Manager { }
public class Handler { }
// GOOD — domain-specific names
public class OrderSummary { }
public class PaymentProcessor { }
public class InvoiceGenerator { }Small, Focused Methods
// BAD — one method doing everything
public async Task ProcessOrderAsync(Order order)
{
// validate
if (order.Items.Count == 0) throw new InvalidOperationException("No items");
if (order.CustomerId <= 0) throw new InvalidOperationException("Invalid customer");
// calculate
decimal subtotal = order.Items.Sum(i => i.Price * i.Quantity);
decimal tax = subtotal * 0.20m;
decimal total = subtotal + tax;
// save
order.Total = total;
await _db.Orders.AddAsync(order);
await _db.SaveChangesAsync();
// notify
await _email.SendAsync(order.CustomerEmail, $"Order confirmed: £{total}");
}
// GOOD — each method has one responsibility
public async Task ProcessOrderAsync(Order order)
{
ValidateOrder(order);
order.Total = CalculateTotal(order);
await _repository.SaveAsync(order);
await _notifier.SendConfirmationAsync(order);
}
private void ValidateOrder(Order order)
{
if (order.Items.Count == 0)
throw new InvalidOperationException("Order must have at least one item");
if (order.CustomerId <= 0)
throw new InvalidOperationException("Order must have a valid customer");
}
private decimal CalculateTotal(Order order)
{
decimal subtotal = order.Items.Sum(i => i.Price * i.Quantity);
return subtotal * 1.20m; // includes 20% VAT
}Guard Clauses (Early Returns)
// BAD — deep nesting
public string GetUserDisplayName(int userId)
{
var user = _db.Users.Find(userId);
if (user != null)
{
if (user.IsActive)
{
if (!string.IsNullOrEmpty(user.DisplayName))
{
return user.DisplayName;
}
}
}
return "Unknown";
}
// GOOD — guard clauses exit early, happy path at the bottom
public string GetUserDisplayName(int userId)
{
var user = _db.Users.Find(userId);
if (user is null) return "Unknown";
if (!user.IsActive) return "Unknown";
if (string.IsNullOrEmpty(user.DisplayName)) return "Unknown";
return user.DisplayName;
}Avoid Magic Numbers and Strings
// BAD — magic numbers with no explanation
if (password.Length < 8)
throw new ArgumentException("Too short");
await Task.Delay(30000);
var discount = price * 0.15m;
// GOOD — named constants communicate intent
private const int MinPasswordLength = 8;
private const int RetryDelayMs = 30_000; // underscore for readability
private const decimal LoyaltyDiscountRate = 0.15m;
if (password.Length < MinPasswordLength)
throw new ArgumentException($"Password must be at least {MinPasswordLength} characters");
await Task.Delay(RetryDelayMs);
var discount = price * LoyaltyDiscountRate;Immutability
// Prefer immutable data — fewer moving parts, easier to reason about
// Mutable record (avoid)
public class Order
{
public int Id { get; set; }
public decimal Total { get; set; }
}
// Immutable record (prefer) — use C# record types
public record Order(int Id, decimal Total, DateTime CreatedAt);
// Create modified copies (non-destructive mutation)
var original = new Order(1, 100m, DateTime.UtcNow);
var updated = original with { Total = 150m }; // new instance
// Immutable collections
using System.Collections.Immutable;
ImmutableList<string> tags = ImmutableList.Create("api", "orders");
ImmutableList<string> withNew = tags.Add("urgent"); // new list
// init-only properties (C# 9+) — set once, then readonly
public class CustomerProfile
{
public required string Email { get; init; }
public required string Name { get; init; }
}DRY vs DAMP in Tests
// DRY (Don't Repeat Yourself) — in production code
// DRY in tests creates hidden coupling and hard-to-read tests
// DAMP (Descriptive And Meaningful Phrases) — in test code
// BAD (overly DRY test):
[Fact]
public void CreateOrder_Valid_Succeeds()
{
var order = BuildTestOrder(); // what's in here? go find it
var result = _service.Create(order);
Assert.True(result.IsSuccess);
}
// GOOD (DAMP test — explicit, self-documenting):
[Fact]
public void CreateOrder_WithValidItems_ReturnsSuccessResult()
{
var order = new Order(
CustomerId: 42,
Items: [new OrderItem("SKU-001", Quantity: 2, UnitPrice: 9.99m)],
DeliveryAddress: "123 Test Street"
);
var result = _service.Create(order);
Assert.True(result.IsSuccess);
Assert.Equal(19.98m, result.Value.Total);
}Interview Answer
"Clean code is about communication: the next developer who reads this code should understand intent without a comment. Key practices: names reveal purpose (
daysUntilExpiry, notd); methods do one thing (single responsibility makes them easy to test and change); guard clauses eliminate deep nesting (exit early for invalid cases, happy path at the bottom); named constants replace magic numbers (reader understands why the number exists); immutability reduces the surface area of bugs (immutable records,init-only properties). DAMP test code is preferable to DRY test code — tests should be explicit and self-documenting rather than abstracting test data into shared helpers. Clean code is not about following rules mechanically; it is about making the code's intent visible."
Enjoyed this article?
Explore the Backend Systems learning path for more.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.