This chapter introduces Pragmatica Core—the library that provides the foundational types for JBCT. You’ll learn why the library exists, its design philosophy, and how to use its core features effectively.
Why a Custom Library?
Java provides Optional and CompletableFuture. Why not use them?
Problems with Java’s Built-in Types
| Type | Problem |
|---|---|
Optional |
No error channel - you know something is missing but not why |
Optional |
Not designed for validation - orElseThrow loses context |
CompletableFuture |
Exceptions as control flow - checked exceptions don’t compose |
CompletableFuture |
Verbose API - thenApply, thenCompose, exceptionally chains |
null |
Billion-dollar mistake - no type safety, NPE at runtime |
What JBCT Needs
- Four distinct return types - Each with clear semantics
- Composable error handling - Errors as values, not exceptions
- Consistent API - Same method names across all types
- Type-safe transformations - Lift between types without losing information
Pragmatica Core provides exactly this.
Installation
Maven (preferred):
<dependency> <groupId>org.pragmatica-lite</groupId> <artifactId>core</artifactId> <version>1.0.0-rc1</version> </dependency>
Gradle:
implementation 'org.pragmatica-lite:core:1.0.0-rc1'
The Four Core Types
Option<T> - Value May Be Absent
import org.pragmatica.lang.Option; Option<User> user = Option.some(new User("Alice")); Option<User> nobody = Option.none(); Option<User> fromNullable = Option.option(possiblyNullValue);
Use when: Value might not exist, but absence is not an error.
Examples: Cache lookup, optional configuration, search result.
Result<T> - Operation May Fail
import org.pragmatica.lang.Result; import org.pragmatica.lang.Cause; Result<Email> valid = Result.success(new Email("[email protected]")); Result<Email> invalid = INVALID_EMAIL.result(); // Preferred: fluent style // Also works (but verbose) Result<Email> invalid2 = Causes.cause("Invalid format").result();
Use when: Synchronous operation that can fail with typed error.
Examples: Validation, parsing, business rule checks.
Promise<T> - Async Operation May Fail
import org.pragmatica.lang.Promise; Promise<User> fetching = Promise.promise(); // Unresolved Promise<User> ready = Promise.success(user); // Already resolved Promise<User> failed = USER_NOT_FOUND.promise(); // Preferred: fluent style
Use when: Asynchronous operation (I/O, external service).
Examples: Database query, HTTP call, file read.
Cause - Typed Error
import org.pragmatica.lang.Cause; import org.pragmatica.lang.utils.Causes; // Simple cause Cause simple = Causes.cause("Something went wrong"); // Cause with context Fn1<Cause, String> INVALID_EMAIL = Causes.forOneValue("Invalid email: %s"); Cause withContext = INVALID_EMAIL.apply("not-an-email"); // Cause from exception Cause fromException = Causes.fromThrowable(exception);
Use when: Representing failure reason in Result or Promise.
Design Philosophy
Consistent Method Names
All types share the same vocabulary where applicable:
| Method | Option | Result | Promise |
|---|---|---|---|
| Transform value | map() |
map() |
map() |
| Chain operations | flatMap() |
flatMap() |
flatMap() |
| Filter with condition | - | filter() |
filter() |
| Provide fallback | or() |
or() |
or() |
| Handle success | onPresent() |
onSuccess() |
onSuccess() |
| Handle failure | onEmpty() |
onFailure() |
onFailure() |
Prefer Cause Methods Over Factory Methods
// DO: Use Cause methods Cause error = VALIDATION_FAILED; return error.result(); // Result<T> return error.promise(); // Promise<T> // DON'T: Use factory methods with Cause return Result.failure(error); // Works but verbose return Promise.failure(error); // Works but verbose
Lift to Higher Types, Not Lower
// Lifting UP is safe Option<T> option = ...; Result<T> result = option.toResult(CAUSE_IF_EMPTY); Promise<T> promise = result.async(); // Going DOWN loses information Result<T> result = ...; Option<T> option = result.option(); // Loses error info!
Type Transformations
Option Conversions
Option<User> option = findUser(id); // Option -> Result (provide cause for empty case) Result<User> result = option.toResult(USER_NOT_FOUND); // Option -> Promise (provide cause for empty case) Promise<User> promise = option.async(USER_NOT_FOUND); // Option -> Java Optional (interop only) Optional<User> javaOptional = option.toOptional();
Result Conversions
Result<User> result = validateUser(input); // Result -> Promise (lift to async context) Promise<User> promise = result.async(); // Result -> Option (loses error - use sparingly) Option<User> option = result.option();
Promise Conversions
Promise<User> promise = fetchUser(id); // Promise -> Result (blocks current thread) Result<User> result = promise.await(); // Promise -> Result with timeout Result<User> result = promise.await(TimeSpan.timeSpan(5).seconds());
Common Operations
map - Transform Success Value
Result<String> raw = Result.success(" [email protected] "); Result<String> normalized = raw .map(String::trim) .map(String::toLowerCase); // Result.success("[email protected]")
flatMap - Chain Dependent Operations
Result<Email> email = Email.email(rawEmail); Result<Password> password = Password.password(rawPassword); // Wrong: Nested Result Result<Result<ValidRequest>> nested = email.map(e -> password.map(p -> new ValidRequest(e, p)) ); // Correct: Flat chain Result<ValidRequest> flat = email.flatMap(e -> password.map(p -> new ValidRequest(e, p)) );
filter - Validate with Predicate
Result<Integer> age = Result.success(25);
Result<Integer> adult = age.filter(
Causes.cause("Must be 18 or older"),
a -> a >= 18
);
recover - Handle Specific Failures
Promise<Config> config = loadFromDatabase()
.recover(cause -> Config.defaults()); // any failure -> built-in defaults
recover’s mapper is synchronous — it turns a failure into a plain value. When the
alternative is itself asynchronous (load from a file, retry, propagate some causes), use
fold or orElse instead.
or / orElse - Provide Fallback
// or: Fallback value String name = option.or("Anonymous"); // orElse: Fallback wrapped value Option<User> user = cache.find(id).orElse(database.find(id));
Aggregation
Result.all - Validate Multiple Fields
Result<ValidRequest> request = Result.all(
Email.email(raw.email()),
Password.password(raw.password()),
Name.name(raw.name())
).map(ValidRequest::new);
Behavior: Accumulates ALL failures into CompositeCause. Does not short-circuit.
Promise.all - Parallel Execution
Promise<Dashboard> dashboard = Promise.all(
fetchProfile(userId),
fetchOrders(userId),
fetchPreferences(userId)
).map(Dashboard::new);
Behavior: Executes in parallel, fails fast on first failure.
Result.allOf / Promise.allOf - Collection Aggregation
// Validate list of emails Result<List<Email>> emails = Result.allOf( rawEmails.stream().map(Email::email).toList() ); // Fetch multiple users in parallel Promise<List<Result<User>>> users = Promise.allOf( userIds.stream().map(this::fetchUser).toList() );
any - First Success Wins
Promise<Config> config = Promise.any(
loadFromPrimarySource(),
loadFromSecondarySource(),
loadFromFallback()
);
Exception Handling with lift
Basic lift - Wrap Throwing Code
// Wrap supplier that may throw Result<Integer> parsed = Result.lift(() -> Integer.parseInt(raw)); // With custom error mapping Result<Integer> parsed = Result.lift( ParseError::new, () -> Integer.parseInt(raw) );
lift with Parameters
// Single parameter Result<Hash> hashed = Result.lift1( HashError::new, encoder::encode, password.value() ); // Two parameters Result<Token> token = Result.lift2( TokenError::new, tokenService::generate, userId, expiry );
Promise.lift - Async Exception Wrapping
Promise<User> user = Promise.lift(
DatabaseError::new,
() -> jdbcTemplate.queryForObject(sql, mapper, id)
);
Validation Utilities
Verify.Is Predicates
import org.pragmatica.lang.utils.Verify; // String checks Verify.ensure(value, Verify.Is::notNull) Verify.ensure(value, Verify.Is::present) // Not null and not blank (CharSequence) Verify.ensure(value, Verify.Is::notBlank) Verify.ensure(value, Verify.Is::notEmpty) // Length checks Verify.ensure(password, Verify.Is::lenBetween, 8, 128) // Pattern matching Verify.ensure(email, Verify.Is::matches, EMAIL_PATTERN) // Numeric checks Verify.ensure(age, Verify.Is::positive) Verify.ensure(age, Verify.Is::between, 0, 150) Verify.ensure(quantity, Verify.Is::lessThanOrEqualTo, 100)
Combining Validations
public static Result<Password> password(String raw) { return Verify.ensure(raw, Verify.Is::present) .filter(TOO_SHORT, s -> Verify.Is.lenBetween(s, 8, 128)) .filter(NO_DIGIT, DIGIT_PATTERN.asMatchPredicate()) .filter(NO_UPPER, UPPER_PATTERN.asMatchPredicate()) .map(Password::new); }
Parse Utilities - JDK Wrappers
import org.pragmatica.lang.parse.*; // Instead of Result.lift(() -> Integer.parseInt(raw)) Result<Integer> number = Number.parseInt(raw); Result<Long> bigNumber = Number.parseLong(raw); Result<BigDecimal> decimal = Number.parseBigDecimal(raw); // Date/time parsing Result<LocalDate> date = DateTime.parseLocalDate(raw); Result<Instant> instant = DateTime.parseInstant(raw); // Network types Result<UUID> uuid = Network.parseUUID(raw); Result<URI> uri = Network.parseURI(raw);
Callback Methods
Success/Failure Handlers
result
.onSuccess(user -> log.info("Found user: {}", user.id()))
.onFailure(cause -> log.warn("User not found: {}", cause.message()));
promise
.onSuccess(data -> cache.put(key, data))
.onFailure(cause -> metrics.incrementFailure());
Result Handler (Both Cases)
promise.onResult(result -> {
if (result.isSuccess()) {
metrics.recordSuccess();
} else {
metrics.recordFailure();
}
});
fold - Transform Both Cases
// Result -> ResponseEntity ResponseEntity<?> response = result.fold( cause -> toErrorResponse(cause), // Failure case first data -> ResponseEntity.ok(data) // Success case second );
Unit Type
For operations that succeed but produce no value:
// Use Unit, never Void Result<Unit> validated = checkBusinessRule(input); Promise<Unit> sent = sendNotification(user); // Create success with no value Result<Unit> ok = Result.unitResult(); // Convert any result to Unit Promise<Unit> ignored = fetchData().mapToUnit();
Thread Safety
Promise Resolution
Promises have exactly-once resolution semantics:
Promise<User> promise = Promise.promise(); // Only first resolution wins promise.succeed(user1); // This resolves the promise promise.succeed(user2); // Ignored promise.fail(cause); // Ignored
Transformation Thread Safety
Transformations are thread-safe but execution order depends on resolution:
Promise<User> promise = fetchUser(id); // These may execute on different threads promise .map(this::enrichUser) // Executes after resolution .onSuccess(this::cacheUser) // Executes after map .onFailure(this::logError); // Executes if failed
Quick Reference
Creating Instances
// Option Option.some(value) // Present Option.none() // Empty Option.option(nullable) // null -> none, value -> some // Result Result.success(value) // Success cause.result() // Failure (preferred) Result.unitResult() // Success with Unit // Promise Promise.success(value) // Resolved success cause.promise() // Resolved failure (preferred) Promise.promise() // Unresolved
Type Lifting
option.toResult(cause) // Option -> Result option.async(cause) // Option -> Promise result.async() // Result -> Promise promise.await() // Promise -> Result (blocks)
Common Transformations
.map(fn) // Transform value .flatMap(fn) // Chain monadic operation .filter(cause, predicate) // Validate with predicate .recover(fn) // Handle failure .or(fallback) // Provide default value .mapToUnit() // Discard value, keep success/failure
Library Value Objects
Pragmatica Core provides production-ready value objects for common types in org.pragmatica.lang.vo. These are explicitly designed to cover common use cases in real business logic:
| Value Object | Description | Factory Method |
|---|---|---|
Email |
RFC 5321 compliant email address | Email.email(String raw) |
Url |
Validated URL with scheme and host | Url.url(String raw) |
Uuid |
UUID with parse and generate | Uuid.uuid(String raw), Uuid.randomUuid() |
NonBlankString |
Trimmed, guaranteed non-empty | NonBlankString.nonBlankString(String raw) |
IsoDateTime |
ISO 8601 datetime with offset | IsoDateTime.isoDateTime(String raw), IsoDateTime.now() |
All factory methods return Result<T>, following the parse-don’t-validate pattern. Use these directly for common types. Build custom value objects for domain-specific types like OrderId, Username, or ReferralCode.
Exercises
-
Parse a date range: Create a
DateRangevalue object withfromandtofields. UseDateTime.parseLocalDate()and validate thatfromis beforeto. -
Combine validations: Write a
Usernamevalue object that must be 3-20 characters, alphanumeric only, and not on a blocklist. Chain the validations. -
Handle nullable external API: You receive a
Map<String, Object>from a JSON parser. Write a method that safely extracts an optionalemailfield and validates it. -
Lift JDBC code: Wrap a
jdbcTemplate.query()call inPromise.lift()with appropriate error mapping.
Summary
Pragmatica Core provides:
- Four types with clear semantics - Option, Result, Promise, Cause
- Consistent API - Same method names across types
- Composable error handling - Errors as values, not exceptions
- Type-safe transformations - Lift between types without losing information
- Validation utilities - Verify.Is predicates, parse wrappers
- Thread-safe promises - Exactly-once resolution, safe transformations
The library is intentionally minimal. It provides the building blocks for JBCT without imposing architectural decisions. Your business logic remains framework-independent.
What’s Next
With the foundation in place, we move to the core principles:
- Parse, Don’t Validate: Making invalid states unrepresentable
- Error Handling & Composition: Typed errors and recovery patterns