Specifications¶
Info
At ConnectSoft, Specifications are a core tactical tool —
enabling us to express complex domain rules, encapsulate query logic,
and preserve domain model purity without leaking infrastructure concerns.
Introduction¶
In Domain-Driven Design (DDD), a Specification represents a business rule or a query criteria
that can be evaluated against one or more domain objects.
Specifications:
- Express complex logic declaratively.
- Enable validation, querying, and composition cleanly.
- Keep business rules inside the domain model, not scattered across services, controllers, or repositories.
- Can be composed dynamically to build powerful, flexible workflows.
At ConnectSoft, Specifications are foundational to:
- Resilient domain integrity validation.
- Dynamic, complex query construction across microservices and SaaS platforms.
- Testable business rule enforcement.
- Enabling CQRS and Event-Driven workflows cleanly.
Concept Definition¶
A Specification:
| Aspect | Description |
|---|---|
| Purpose | Encapsulate a business rule or query predicate. |
| Type | Can be executable (in-memory validation) or translatable (e.g., to LINQ, SQL, etc.). |
| Composability | Specifications can be combined using logical operations (AND, OR, NOT). |
| Reusability | Specifications can be reused across Aggregates, Services, and Repositories. |
| Testability | Specifications are independently unit-testable. |
📚 Why Specifications Matter at ConnectSoft¶
✅ Preserve Domain Purity
- Business logic stays inside the domain model, not scattered across layers.
✅ Enable Dynamic Querying
- Powerful query capabilities without bloating Repository interfaces.
✅ Promote Reuse and Composition
- Specifications can be combined dynamically depending on use cases.
✅ Support CQRS and Event-Driven Designs
- Query and validation logic remains structured and evolvable.
✅ Simplify Validation Logic
- Domain services and Aggregates validate complex business rules easily using Specification evaluations.
Specification vs Ad-hoc Queries or Validations¶
| Aspect | Specification | Ad-hoc Validation |
|---|---|---|
| Separation of Concerns | Business rule expressed independently | Rule embedded into service, controller, or repository |
| Composability | Can be composed (AND, OR, NOT) | Hard to compose or reuse |
| Testability | Easy to test in isolation | Tightly coupled, hard to test |
| Reusability | Highly reusable across services, use cases | Duplicated across codebase |
| Evolution Readiness | Easy to extend dynamically | Hard to extend without rewriting logic |
🧩 Visual: Specification Across Domain Layers¶
flowchart TD
UI["UI/API Layer"]
AppService["Application Service"]
Specification["Domain Specification (Business Rule)"]
Repository["Repository"]
Database["Database"]
UI --> AppService
AppService --> Specification
Specification --> Repository
Repository --> Database
✅ Application Services invoke Specifications directly or pass them into Repositories.
✅ Specifications remain inside the Domain Layer, governing logic cleanly.
Strategic Design Principles for Specifications¶
At ConnectSoft, we design Specifications to be clean, composable, reusable, and tactical —
ensuring that business rules stay isolated, expressive, and testable.
📚 Core Principles for Good Specification Design¶
✅ Keep Specifications Stateless
- They should express logic without holding state — ensuring predictability and reusability.
✅ Encapsulate One Clear Rule Per Specification
- A Specification should encapsulate exactly one business rule or query intent.
✅ Support Composition Natively
- Enable combining Specifications easily via AND, OR, and NOT operators.
✅ Evaluate Locally or Translate Remotely
- Specifications should be evaluatable both in-memory and translatable to database queries (LINQ, SQL, ElasticSearch, etc.).
✅ Design for Reuse
- A good Specification can be reused across multiple Aggregates, Repositories, or Application Services without modification.
✅ Enable Unit Testing in Isolation
- Each Specification should be unit-tested independently.
✅ Expose Meaningful Domain Language
- Specification names should reflect business rules clearly (e.g.,
IsPremiumCustomer,HasOutstandingBalance,IsAppointmentOverlapping).
📚 Specification Composition Techniques¶
Specifications must support logical operations safely and fluently.
| Composition | Example |
|---|---|
| AND Composition | Find active customers AND customers with loyalty points above 100. |
| OR Composition | Find orders that are Pending OR AwaitingPayment. |
| NOT Composition | Find appointments that are NOT yet confirmed. |
C# Composition Example (Tactical)¶
public interface ISpecification<T>
{
Expression<Func<T, bool>> ToExpression();
}
// Combinator
public static class SpecificationExtensions
{
public static ISpecification<T> And<T>(this ISpecification<T> left, ISpecification<T> right)
{
return new AndSpecification<T>(left, right);
}
public static ISpecification<T> Or<T>(this ISpecification<T> left, ISpecification<T> right)
{
return new OrSpecification<T>(left, right);
}
public static ISpecification<T> Not<T>(this ISpecification<T> specification)
{
return new NotSpecification<T>(specification);
}
}
✅ Fluent, composable Specifications
✅ Chain AND, OR, NOT operators easily.
📚 Practical Composition Usage Example¶
✅ Find customers who are both active AND have high loyalty points.
✅ No manual filtering in services or repositories!
🛑 Common Anti-Patterns to Avoid with Specifications¶
| Anti-Pattern | Symptom | Why It's Dangerous |
|---|---|---|
| Procedural Validation | Logic spread across services without reusable Specifications. | Hard to test, maintain, or extend. |
| Specification God Objects | Huge Specifications modeling multiple unrelated rules. | Breaks SRP, hard to reuse or compose. |
| Stateful Specifications | Specification result depends on internal mutable fields. | Unpredictable, hard to reason about. |
| Repository-Leaking Specifications | Specification tied to database-specific operations (e.g., raw SQL inside spec). | Breaks domain isolation, kills portability. |
| Duplicated Specifications | Copy-pasted filters or rules in multiple places. | Increases maintenance cost and bugs. |
📚 Good vs Bad Specification Examples¶
| ✅ Good Specification | 🚫 Bad Specification |
|---|---|
| Models one clear domain rule | Bundles many unrelated checks |
| Stateless, pure logic | Holds mutable fields affecting results |
| Easily composed (AND, OR, NOT) | Hard-coded logic without composition support |
| Portable across contexts (validation, query) | Tied to database or API infrastructure |
| Fluent, meaningful names | Generic "Filter1", "Validator2" style names |
🧩 Visual: Composing Specifications Graphically¶
flowchart TD
ActiveCustomer["IsActiveCustomerSpecification"]
HighPoints["HasHighLoyaltyPointsSpecification"]
Composite["Active AND High Points"]
ActiveCustomer --> Composite
HighPoints --> Composite
✅ Specifications combine safely to create higher-order business logic
✅ No duplication or fragile scripting inside services.
C# Examples: Real-World Specification Modeling at ConnectSoft¶
At ConnectSoft, Specifications are used across all core domains —
modeling complex queries and validations without bloating services or repositories.
🛠️ Example 1: E-Commerce — OrderStatusSpecification¶
public class OrderStatusSpecification : ISpecification<Order>
{
private readonly OrderStatus _status;
public OrderStatusSpecification(OrderStatus status)
{
_status = status;
}
public Expression<Func<Order, bool>> ToExpression()
{
return order => order.Status == _status;
}
}
✅ Clear domain rule: Filter orders by their status.
✅ Can be composed with other Specifications.
🛠️ Example 2: Finance — HighBalanceSpecification¶
public class HighBalanceSpecification : ISpecification<BankAccount>
{
private readonly decimal _threshold;
public HighBalanceSpecification(decimal threshold)
{
_threshold = threshold;
}
public Expression<Func<BankAccount, bool>> ToExpression()
{
return account => account.Balance >= _threshold;
}
}
✅ Flexible: threshold passed dynamically.
✅ Used to query VIP accounts, calculate interest tiers, etc.
🛠️ Example 3: Healthcare — UpcomingAppointmentsSpecification¶
public class UpcomingAppointmentsSpecification : ISpecification<Appointment>
{
private readonly DateTime _fromDate;
public UpcomingAppointmentsSpecification(DateTime fromDate)
{
_fromDate = fromDate;
}
public Expression<Func<Appointment, bool>> ToExpression()
{
return appointment => appointment.StartTime >= _fromDate;
}
}
✅ Supports building dynamic appointment lists for dashboards, reminders, etc.
📚 Composing Complex Queries Using Specifications¶
Example: Find Active Customers with High Loyalty Points¶
var activeCustomers = new ActiveCustomerSpecification()
.And(new HighLoyaltyPointsSpecification(100));
- Combines two Specifications using AND.
- Can be passed into Repository to build database queries dynamically.
📚 Using Specifications in Repositories¶
public interface ICustomerRepository
{
Task<IEnumerable<Customer>> FindAsync(ISpecification<Customer> specification);
}
public class CustomerRepository : ICustomerRepository
{
private readonly DbContext _context;
public CustomerRepository(DbContext context)
{
_context = context;
}
public async Task<IEnumerable<Customer>> FindAsync(ISpecification<Customer> specification)
{
return await _context.Set<Customer>()
.Where(specification.ToExpression())
.ToListAsync();
}
}
✅ Clean separation: Repository knows nothing about hardcoded queries.
✅ Business rule lives purely in the Specification.
📚 Validation with Specifications Inside Domain Services¶
public class AccountService
{
private readonly IAccountRepository _accountRepository;
public AccountService(IAccountRepository accountRepository)
{
_accountRepository = accountRepository;
}
public async Task CloseAccountAsync(Guid accountId)
{
var account = await _accountRepository.GetByIdAsync(accountId);
var canCloseSpec = new AccountClosableSpecification();
if (!canCloseSpec.ToExpression().Compile().Invoke(account))
throw new InvalidOperationException("Account cannot be closed while balance is non-zero.");
account.Close();
await _accountRepository.UpdateAsync(account);
}
}
public class AccountClosableSpecification : ISpecification<BankAccount>
{
public Expression<Func<BankAccount, bool>> ToExpression()
{
return account => account.Balance == 0 && !account.IsClosed;
}
}
✅ Domain Services validate using reusable Specifications.
✅ No magic conditions hardcoded inside services.
📚 Lessons from Real-World Specification Modeling¶
| Good Practice | Why It Matters |
|---|---|
| Keep specifications lightweight and focused | Makes composition easy |
| Compose dynamically in Application Services or Repositories | Supports flexible business workflows |
| Validate domain rules inside Aggregates/Services with Specifications | Prevents invariant violations |
| Use fluent chaining (AND, OR, NOT) | Improves readability and flexibility |
| Separate querying logic cleanly from domain operations | Keeps architecture layered and maintainable |
🧩 Visual: Specification Usage Across Query and Validation¶
flowchart TD
Specification1["ActiveCustomerSpecification"]
Specification2["HighLoyaltyPointsSpecification"]
Composite["AND Composition"]
Specification1 --> Composite
Specification2 --> Composite
Composite --> Repository["CustomerRepository.FindAsync()"]
✅ Clean flow: Compose business rules ➔ Query via Repositories.
✅ No bloated Application Services.
Best Practices for Specifications¶
At ConnectSoft, Specifications are modeled strategically to ensure that
business rules are clean, reusable, testable, and naturally evolve with the domain.
📚 Best Practices Checklist¶
✅ One Responsibility per Specification
- Each Specification models a single clear rule or condition.
✅ Support Logical Composition
- Specifications should be combinable with AND, OR, and NOT operators.
✅ Remain Stateless
- No mutable state or runtime side-effects inside Specifications.
✅ Be Easily Testable
- Unit tests should validate Specification behavior independently.
✅ Work for Queries and In-Memory Validations
- Same Specification can be used for querying repositories and validating domain invariants.
✅ Isolate from Infrastructure
- Specifications operate on domain objects, not tied to persistence concerns like SQL syntax or EF-specific constructs.
✅ Fluent API for Readability
- Specification compositions (chained expressions) must read naturally and clearly.
✅ Avoid Overloading
- Do not build massive "mega specifications" combining unrelated rules.
✅ Expose Meaningful Names
- Specification class names must reflect the business rule they model.
✅ Version Carefully if Exposed Outside
- If Specifications power APIs or Event Streams, handle backward compatibility cautiously.
Conclusion¶
Specifications at ConnectSoft are not minor helpers —
they are tactical first-class citizens of clean, scalable Domain-Driven systems.
When modeled properly:
- Systems express complex business logic cleanly without cluttering Services or Repositories.
- Queries and Validations share logic, reducing duplication and fragility.
- New workflows evolve by composing existing building blocks, not by rewriting services.
- Testing becomes easier, faster, and more resilient to change.
- Business rule documentation emerges naturally from the codebase.
When neglected:
- Services bloat with if-else chaos.
- Queries leak domain assumptions into infrastructure.
- Business rules become fragile, hidden, and duplicated.
- Scaling and changing systems becomes dangerous.
At ConnectSoft, Specifications empower our developers to build
adaptive, expressive, domain-driven architectures —
ready to evolve with our businesses, industries, and customers.
"Specifications are not filters —
they are the language of business rules,
flowing naturally through code and system behavior."
References¶
-
Books and Literature
- Eric Evans — Domain-Driven Design: Tackling Complexity in the Heart of Software
- Vaughn Vernon — Implementing Domain-Driven Design
- Jimmy Nilsson — Applying Domain-Driven Design and Patterns
-
Online Resources
-
ConnectSoft Internal Standards
- ConnectSoft Specification Pattern Playbook
- ConnectSoft Resilient Querying and Domain Validation Strategies
- ConnectSoft CQRS and Specification Composition Guidelines