A Kotlin implementation of the Specification Pattern for composable, reusable business rules.
Stop scattering validation logic across your codebase. Spec4j lets you define small, testable business rules as specifications and combine them into policies — making your domain logic explicit, reusable, and easy to reason about.
Spec4j provides a clean, type-safe way to define and evaluate business rules. Instead of scattering validation logic throughout your codebase, you define atomic specifications that can be composed into policies.
// Define specifications
val isAdult = Specification.of<User, Reason>("IsAdult", { it.age >= 18 }, Reason.UNDERAGE)
val hasVerifiedEmail = Specification.of<User, Reason>("HasVerifiedEmail", { it.emailVerified }, Reason.EMAIL_NOT_VERIFIED)
// Compose into a policy
val registrationPolicy = Policy.create<User, Reason>()
.with(isAdult)
.with(hasVerifiedEmail)
// Evaluate
val result = registrationPolicy.evaluateFailFast(user)
if (result.allPassed) {
// proceed
} else {
// handle result.failureReasons()
}- 🧩 Composable — Build complex rules from simple, reusable specifications
- 🔒 Type-safe — Failure reasons are enums, not strings
- ⚡ Two evaluation modes —
evaluateFailFast(stops on first failure) orevaluateAll(collects all failures) - 🔗 Logical operators —
allOf,anyOf,notfor combining specifications - 💜 Kotlin-first — DSL builder, invoke operator, and
fun interfacefor idiomatic Kotlin - ☕ Java-friendly — Full interoperability with
@JvmStaticannotations
dependencies {
implementation("io.github.caik:spec4j-core:$version")
}dependencies {
implementation "io.github.caik:spec4j-core:$version"
}<dependency>
<groupId>io.github.caik</groupId>
<artifactId>spec4j-core</artifactId>
<version>$version</version>
</dependency>A single, atomic condition that evaluates a context and returns pass/fail with a reason:
val minimumAge = Specification.of<LoanApplication, Reason>(
"MinimumAge",
{ it.applicantAge >= 18 },
Reason.APPLICANT_TOO_YOUNG
)A named collection of specifications that together define a business rule:
// Fluent builder — works in both Java and Kotlin
val loanEligibility = Policy.create<LoanApplication, Reason>()
.with(minimumAge)
.with(maximumAge)
.with(creditCheck)
// Kotlin DSL — more concise, Kotlin only
val loanEligibility = policy<LoanApplication, Reason> {
+minimumAge
+maximumAge
+creditCheck
}Combine specifications with logical operators:
// AND — all must pass
val fullyVerified = SpecificationFactory.allOf("FullyVerified", emailVerified, phoneVerified)
// OR — at least one must pass
val hasPaymentMethod = SpecificationFactory.anyOf("HasPayment", hasCreditCard, hasBankAccount)
// NOT — inverts the result
val notBlocked = SpecificationFactory.not("NotBlocked", Reason.BLOCKED, isBlockedCountry)The examples module contains complete working examples showcasing Spec4j's capabilities:
# Loan eligibility (basic usage)
./gradlew :examples:run -PexampleName=LoanEligibility
# E-commerce order validation (custom specs, not(), reusable specs)
./gradlew :examples:run -PexampleName=OrderValidation
# Feature access control (multiple policies, dynamic specs)
./gradlew :examples:run -PexampleName=FeatureAccess# Build and run tests
./gradlew build
# Run tests with coverage report
./gradlew test jacocoTestReport
# Report: lib/build/reports/jacoco/test/html/index.htmlContributions are welcome! Please see CONTRIBUTING.md for guidelines.
Released 2026 by Carlos Henrique Severino