When building software that stands the test of time, one principle rises above the rest: keeping your business logic pure. Let’s dive deep into what Domain Layer Purity means and why it’s crucial for maintainable software.
What is Domain Layer Purity?
Domain Layer Purity refers to keeping the domain layer (also called the business logic layer or core layer) completely independent of external concerns like frameworks, databases, UI, external services, or infrastructure details.
Think of it as the heart of your application – it beats independently, unaffected by what’s happening around it.
The Three Pillars of Domain Purity
1. No External Dependencies
The domain layer should contain:
- Pure business logic – Rules that define how your business works
- Domain entities – Core business objects
- Domain services – Business operations that don’t naturally fit in entities
- Value objects – Immutable objects defined by their values
- Domain events – Things that happen in the business domain
The domain layer should NOT depend on:
- Database frameworks (Entity Framework, Hibernate, etc.)
- Web frameworks (Express, Spring MVC, ASP.NET, etc.)
- External APIs or services
- UI frameworks
- Infrastructure concerns (file systems, logging, etc.)
- Third-party libraries (except perhaps basic utilities)
2. Dependency Direction: The Inward Flow
In Clean Architecture, dependencies flow inward toward the domain. Here’s how the layers relate:
┌─────────────────┐
│ Presentation │ (UI, Controllers, APIs)
│ Layer │
└────────┬────────┘
│ depends on
▼
┌─────────────────┐
│ Application │ (Use Cases, Application Services)
│ Layer │
└────────┬────────┘
│ depends on
▼
┌─────────────────┐
│ Domain │ ◄─── PURE (no outward dependencies)
│ Layer │
└─────────────────┘
▲
│ depends on
│
┌────────┴────────┐
│ Infrastructure │ (Database, External APIs, File System)
│ Layer │
└─────────────────┘
Key takeaway: The domain layer sits at the center and has NO dependencies pointing outward.
3. Why Purity Matters
Business Logic Isolation
- Your core business rules remain stable even when technology changes
- Easy to understand what the business actually does
- No framework clutter obscuring business intent
Testability
Pure domain code is incredibly easy to test. Here’s an example:
// Pure domain entity - easy to test
public class Order
{
private readonly List<OrderItem> _items;
private readonly Customer _customer;
public OrderStatus Status { get; private set; }
public decimal Discount { get; private set; }
public Order(List<OrderItem> items, Customer customer)
{
_items = items;
_customer = customer;
Status = OrderStatus.Pending;
}
public decimal CalculateTotal()
{
return _items.Sum(item => item.Price);
}
public void ApplyDiscount(decimal discountPercentage)
{
if (discountPercentage < 0 || discountPercentage > 100)
{
throw new ArgumentException("Invalid discount percentage");
}
Discount = discountPercentage;
}
public bool CanBeCancelled()
{
return Status == OrderStatus.Pending || Status == OrderStatus.Confirmed;
}
}
// No database, no framework - just pure logic testing
[Test]
public void ShouldCalculateOrderTotalCorrectly()
{
var items = new List<OrderItem>
{
new OrderItem { Price = 10 },
new OrderItem { Price = 20 }
};
var order = new Order(items, customer);
Assert.AreEqual(30, order.CalculateTotal());
}
Flexibility and Longevity
- Swap databases without touching business logic
- Change UI frameworks without affecting core rules
- Switch from REST to GraphQL without domain changes
- Business rules outlive technology choices
Pure vs. Impure: Real Examples
Example 1: The Right Way (Pure Domain Code)
// Pure domain entity
public class BankAccount
{
public string AccountNumber { get; private set; }
public decimal Balance { get; private set; }
public BankAccount(string accountNumber, decimal balance = 0)
{
AccountNumber = accountNumber;
Balance = balance;
}
public void Deposit(decimal amount)
{
if (amount <= 0)
{
throw new ArgumentException("Deposit amount must be positive");
}
Balance += amount;
}
public void Withdraw(decimal amount)
{
if (amount <= 0)
{
throw new ArgumentException("Withdrawal amount must be positive");
}
if (amount > Balance)
{
throw new InvalidOperationException("Insufficient funds");
}
Balance -= amount;
}
public bool CanWithdraw(decimal amount)
{
return amount > 0 && amount <= Balance;
}
}
// Pure domain service
public class TransferService
{
public void Transfer(BankAccount fromAccount, BankAccount toAccount, decimal amount)
{
if (!fromAccount.CanWithdraw(amount))
{
throw new InvalidOperationException("Cannot transfer: insufficient funds");
}
fromAccount.Withdraw(amount);
toAccount.Deposit(amount);
}
}
What makes this pure?
- No external dependencies
- Pure C# – no framework
- Testable without any infrastructure
- Business logic is crystal clear
Example 2: The Wrong Way (Impure Domain Code)
// BAD: Domain entity coupled to database
using Microsoft.EntityFrameworkCore; // ❌ External dependency
using System.Net.Http; // ❌ External dependency
using Serilog; // ❌ External dependency
[Table("BankAccounts")] // ❌ Framework attribute in domain
public class BankAccount : DbContext // ❌ Inheriting from infrastructure
{
[Column] // ❌ Database concern in domain
public string AccountNumber { get; set; }
[Column] // ❌ Database concern in domain
public decimal Balance { get; set; }
private readonly HttpClient _httpClient; // ❌ Infrastructure in domain
private readonly ILogger _logger; // ❌ Infrastructure in domain
public async Task WithdrawAsync(decimal amount) // ❌ Async for database in domain logic
{
if (amount > Balance)
{
throw new InvalidOperationException("Insufficient funds");
}
Balance -= amount;
// ❌ Direct database call in domain
await SaveChangesAsync();
// ❌ External API call in domain
await _httpClient.PostAsJsonAsync("/notifications", new
{
message = "Withdrawal made"
});
// ❌ Logging infrastructure in domain
_logger.Information("Withdrawal completed");
}
}
What’s wrong here?
- Tightly coupled to Entity Framework Core
- Direct HTTP calls in business logic
- Database operations mixed with business rules
- Infrastructure logging in domain
- Impossible to test without database and external services
How to Maintain Domain Purity
Technique 1: Use Interfaces and Abstractions
Instead of calling infrastructure directly, define what you need:
// Domain layer defines the interface (port)
public interface IEmailSender
{
void SendEmail(string to, string subject, string body);
}
// Domain service uses the abstraction
public class OrderService
{
private readonly IEmailSender _emailSender;
public OrderService(IEmailSender emailSender)
{
_emailSender = emailSender;
}
public void PlaceOrder(Order order)
{
// Business logic
order.MarkAsPlaced();
// Use abstraction - don't know or care about implementation
_emailSender.SendEmail(
order.Customer.Email,
"Order Confirmation",
$"Your order #{order.Id} is confirmed"
);
}
}
// Infrastructure layer provides the implementation (adapter)
public class SmtpEmailSender : IEmailSender
{
public void SendEmail(string to, string subject, string body)
{
// Actual SMTP implementation here
// Uses SmtpClient, MailKit, or SendGrid
}
}
This is the famous Dependency Inversion Principle in action!
Technique 2: Keep Persistence Ignorance
Domain entities shouldn’t know how they’re stored:
// ✅ Pure domain entity
public class Product
{
public Guid Id { get; private set; }
public string Name { get; private set; }
public decimal Price { get; private set; }
public Product(Guid id, string name, decimal price)
{
Id = id;
Name = name;
Price = price;
}
public void ApplyDiscount(decimal percentage)
{
Price = Price * (1 - percentage / 100);
}
}
// Infrastructure handles persistence separately
public class ProductRepository
{
private readonly DbContext _context;
public ProductRepository(DbContext context)
{
_context = context;
}
public void Save(Product product)
{
// Database logic here - entity doesn't know or care
_context.Products.Add(new ProductEntity
{
Id = product.Id,
Name = product.Name,
Price = product.Price
});
_context.SaveChanges();
}
}
Technique 3: No Framework Artifacts
Keep framework-specific code out of domain:
// ❌ BAD: Framework in domain
using System.ComponentModel.DataAnnotations; // Framework dependency
public class User : EntityBase // Framework base class
{
[Required] // Framework attribute
[EmailAddress] // Framework attribute
public string Email { get; set; }
}
// ✅ GOOD: Pure domain
public class User
{
public string Email { get; private set; }
public User(string email)
{
if (!IsValidEmail(email))
{
throw new ArgumentException("Invalid email");
}
Email = email;
}
private bool IsValidEmail(string email)
{
// Pure validation logic
var emailPattern = @"^[^\s@]+@[^\s@]+\.[^\s@]+$";
return System.Text.RegularExpressions.Regex.IsMatch(email, emailPattern);
}
}
Red Flags: Spotting Violations of Purity
Watch out for these warning signs in your domain layer:
- Import statements for databases, ORMs, or web frameworks in domain files
- Async/await driven by infrastructure needs (not business requirements)
- HTTP concerns in domain (status codes, headers, etc.)
- Direct file system access
- Configuration reading in domain logic
- Framework decorators/annotations on domain entities
- Infrastructure logging in domain methods
Benefits: Why This Matters
- Testability – Test business logic without infrastructure
- Maintainability – Business rules in one clear place
- Flexibility – Swap technologies without touching core
- Longevity – Business logic outlives framework choices
- Clarity – Pure code is easier to understand
- Portability – Move to different platforms easily
The Golden Rule
The domain layer should be so pure that you could theoretically run it on any platform, with any database, any UI, and any infrastructure – the business rules remain the same.
Practical Exercise
Look at your current codebase and ask yourself:
- Can I test my core business logic without spinning up a database?
- If I switched from MySQL to MongoDB, how much of my domain would change?
- Are my business rules clearly visible, or are they buried in framework code?
- Could I port my business logic to a different language/platform easily?
If you answered “no” to any of these questions, you might have some purity violations to address.
Conclusion
Domain Layer Purity isn’t just a theoretical concept – it’s a practical approach to building software that lasts. By keeping your business logic independent of external concerns, you create systems that are:
- Easier to test
- Simpler to maintain
- More flexible to change
- Longer-lasting
Start small. Pick one domain entity in your current project and refactor it to be pure. You’ll immediately see the benefits in clarity and testability. Then expand from there.
Remember: frameworks come and go, but your business logic should remain constant. Keep it pure, keep it simple, and your future self will thank you.
Have you implemented Clean Architecture in your projects? What challenges did you face with domain purity? Share your experiences in the comments below!
