Specification
Specification represents a business rule as an object that can be combined with other rules. It is useful when eligibility or filtering rules are reused in several places.
iOS use case
A product picker should show items that are in stock, available in the user's region, and optionally under a spending limit.
import Foundation
struct Product {
let price: Decimal
let stock: Int
let regions: Set<String>
}
struct Specification<Value> {
let matches: (Value) -> Bool
func and(_ other: Specification<Value>) -> Specification<Value> {
Specification { matches($0) && other.matches($0) }
}
}
let inStock = Specification<Product> { $0.stock > 0 }
let availableInIndia = Specification<Product> { $0.regions.contains("IN") }
let underBudget = Specification<Product> { $0.price <= 500 }
let eligible = inStock.and(availableInIndia).and(underBudget)
let product = Product(price: 299, stock: 4, regions: ["IN", "SG"])
print(eligible.matches(product)) // trueThe combined rule reads like the feature requirement and can be shared by several local views.
Production decisions
The server must still enforce purchase eligibility; local specifications improve browsing feedback. If the list is paginated, apply the same rule in the backend query rather than filtering only loaded items. Test boundary prices, zero stock, and region changes. A few one-off conditions do not need a specification abstraction.
Build a rule from smaller rules
Specification<Value> wraps one predicate and and(_:) creates a new specification that requires both predicates to pass. inStock, availableInIndia, and underBudget are independently named business rules. Their composition reads like the eligibility requirement, and a caller can reuse or replace one rule without rewriting a large if expression. The generic type prevents accidentally combining a product rule with a rule for a different domain model.
From example to an iOS catalog
The view model can filter a local preview set for instant feedback as a user changes region or budget. If the catalog is paginated or server-backed, express the same policy in the backend query and enforce it again at purchase time; filtering one loaded page would hide eligible items on other pages. Define whether budget includes tax and shipping, and represent money in a known currency before comparing Decimal values. Domain wording matters more than the abstraction itself.
Failure modes and verification
Test zero stock, exact budget boundary, missing region, and combinations in different orders. A closure-based specification cannot automatically be translated into SQL; a richer expression tree would be needed for that. Use this pattern when rules are reused and composed. A one-off Boolean condition is clearer as a direct expression. Keep security-critical eligibility authoritative on the server.