| name | solid-clean-code |
| description | Canonical SOLID and Clean Code rules for backend tasks. Load on every backend task.
|
| requires | ["backend-dotnet-csharp"] |
| produces | ["method-design","class-boundaries","complexity-control"] |
SOLID Principles & Clean Code — TemperAI
🚨 NON-NEGOTIABLE RULES — ZERO TOLERANCE
- NEVER write a method with multiple unrelated responsibilities
- NEVER hide the happy path behind deep nesting — use early returns
- NEVER add new behavior by mutating an existing use case — create a new use case class
- NEVER keep dead code, commented code, or unused imports
- NEVER use boolean parameters when separate methods would express intent better
- NEVER require comments to explain WHAT the code does — comments are for WHY only
Practical guidance
- Keep straightforward use case flows inline when they remain readable
- Extract private methods only when they improve clarity, not mechanically
- Split classes when responsibilities diverge
- Prefer descriptive names over explanatory comments
The use case flow — readability first
A typical use case flow such as validate -> create -> persist -> return is one cohesive job. Keep that flow inline when each step is straightforward.
public async Task<Result<CreateProductResponseDto>> ExecuteAsync(
CreateProductRequestDto request,
CancellationToken ct)
{
List<string> errors = Validate(request);
if (errors.Count > 0)
return Result<CreateProductResponseDto>.Failure(HttpStatusCode.BadRequest)
.WithErrors(errors);
Product product = new()
{
Id = Guid.NewGuid(),
Name = request.Name,
Price = request.Price,
CreatedAt = DateTime.UtcNow
};
await _productRepository.AddAsync(product, ct);
await _unitOfWork.CompleteAsync(ct);
return Result<CreateProductResponseDto>.Success(HttpStatusCode.Created)
.WithPayload(product.ToCreateProductResponseDto());
}
Extract only when:
- A block has its own internal complexity
- The same logic appears in multiple places
- The extracted name makes the caller more readable
SOLID application rules
SRP — single responsibility
- One class, one reason to change
- One method, one cohesive job
- If you describe a class with "and", it is probably doing too much
public sealed class CreateOrder : ICreateOrder { }
public sealed class SendOrderConfirmation : ISendOrderConfirmation { }
public sealed class CreateOrder : ICreateOrder
{
public async Task<Result<CreateOrderResponseDto>> ExecuteAsync(...)
{
await _orderRepository.AddAsync(order, ct);
await _emailSender.SendAsync(order.CustomerEmail, "Order confirmed", ct);
await _invoiceGenerator.GenerateAsync(order.Id, ct);
return Result<CreateOrderResponseDto>.Success(HttpStatusCode.Created);
}
}
OCP — open for extension, closed for modification
- New behavior means new file or new use case
- New query need means new repository method
- New response shape means new DTO
if (request.IsBulkImport)
{
}
public sealed class CreateProduct : ICreateProduct { }
public sealed class BulkImportProducts : IBulkImportProducts { }
LSP and ISP
- Never throw
NotImplementedException from repository overrides just because a base contract does not fit
- If the base abstraction does not fit, define a narrower interface
- Split fat interfaces so callers depend only on what they use
DIP
- Use cases depend on interfaces, not infrastructure classes
- Introduce a new abstraction when a use case needs an external capability that should vary by implementation
Clean code rules
Keep code flat with early returns
if (request != null)
{
if (request.Items.Count > 0)
{
foreach (OrderItemDto item in request.Items)
{
if (item.Quantity > 0)
{
}
}
}
}
if (request is null)
return Result<OrderDto>.Failure(HttpStatusCode.BadRequest);
if (request.Items.Count == 0)
return Result<OrderDto>.Failure(HttpStatusCode.BadRequest)
.WithErrors(["Order must have items"]);
Boolean parameters hide intent
Task<Result<List<ProductDto>>> GetProductsAsync(bool includeInactive, CancellationToken ct);
Task<Result<List<ProductDto>>> GetActiveProductsAsync(CancellationToken ct);
Task<Result<List<ProductDto>>> GetAllProductsAsync(CancellationToken ct);
Comments only for why
if (response.Price == -1)
return Result<ProductDto>.Failure(HttpStatusCode.ServiceUnavailable);
Avoid primitive obsession
If a method takes many related primitive arguments, group them into a request DTO.
Anti-patterns — never do this
public sealed class OrderManager
{
public Task<Order> CreateOrder() => throw new NotImplementedException();
public Task SendConfirmationEmail() => throw new NotImplementedException();
public Task ChargeCreditCard() => throw new NotImplementedException();
}
public Task<Result<ProductDto>> UpsertProductAsync(
CreateProductRequestDto? createRequest,
UpdateProductRequestDto? updateRequest,
bool isCreate,
bool skipValidation,
CancellationToken ct) => throw new NotImplementedException();