Archetypy Oprogramowania implements several key design patterns consistently across all modules. Understanding these patterns is essential for working with the codebase effectively.
These aren’t just academic patterns - every example on this page comes directly from the production source code.
private Result<String, AccountId> createAccount( AccountId accountId, AccountType type, AccountName name) { // Check if account already exists if (accountRepository.find(accountId).isPresent()) { return Result.failure( "Account with id " + accountId + " already exists" ); } // Create and save account Account account = new Account(accountId, type, name, Version.initial()); accountRepository.save(account); return Result.success(account.id());}
Why Result instead of Exception?
Account already existing is an expected business case, not an exceptional condition
Caller can easily handle both paths without try-catch
Error message is type-safe (String) and can be displayed to users
Example 2: Chaining Operations with flatMap
Pattern: Creating accounts with initial balances requires two operationsFrom:accounting/src/main/java/com/softwarearchetypes/accounting/AccountingFacade.java:96
public Result<String, Set<AccountId>> createAccountsWithInitialBalances( Set<CreateAccount> requests, AccountAmounts accountAmounts) { // First operation: create accounts Result<String, Set<AccountId>> creation = createAccounts(requests); // Second operation: only if creation succeeded, initialize balances Result<String, TransactionId> txResult = creation.flatMap(ids -> { Transaction transaction = transactionBuilderFactory.transaction() .withTypeOf(INITIALIZATION) .occurredAt(clock.instant()) .appliesAt(clock.instant()) .executing() .entriesFor(accountAmounts) .build(); return execute(transaction); }); // Return original account IDs if transaction succeeded, error otherwise if (txResult.success()) { return creation; } else { return Result.failure(txResult.getFailure()); }}
Key insight:flatMap chains operations that each return Result, automatically propagating failures.
Example 3: Composite Results (Multiple Operations)
Pattern: Accumulating results from multiple independent operationsFrom:accounting/src/main/java/com/softwarearchetypes/accounting/AccountingFacade.java:186
public Result<String, Set<TransactionId>> execute( Transaction... transactions) { CompositeSetResult<String, TransactionId> result = Result.compositeSet(); for (Transaction transaction : transactions) { result = result.accumulate(execute(transaction)); // Fail-fast: stop on first failure if (result.failure()) { return result.toResult(); } } return result.toResult();}
Implementation detail from common/src/main/java/com/softwarearchetypes/common/Result.java:280:
public CompositeSetResult<F, S> accumulate(Result<F, S> newResult) { checkNotNull(newResult, "newResult cannot be null"); if (result.failure()) { return this; // Already failed, stay failed } if (newResult.failure()) { return new CompositeSetResult<>(newResult.getFailure()); } // Both succeeded - add to set Set<S> accumulated = new HashSet<>(result.getSuccess()); accumulated.add(newResult.getSuccess()); return new CompositeSetResult<>(accumulated);}
Every module exposes exactly ONE public entry point - a Facade class:
public class {Module}Facade { // All dependencies are package-private or private private final {Module}Repository repository; private final EventPublisher eventPublisher; // Constructor for dependency injection {Module}Facade(...dependencies) { ... } // Public API: Commands public Result<Error, Id> handle(CreateCommand cmd) { ... } public Result<Error, Id> handle(UpdateCommand cmd) { ... } // Public API: Queries public Optional<View> findById(Id id) { ... } public List<View> findAll() { ... }}
Impossible to call build() before specifying required type-specific attributes
TransactionBuilder - Fluent State Machine
From:accounting/src/main/java/com/softwarearchetypes/accounting/TransactionBuilder.javaThe transaction builder enforces business rules through its API:
Transaction transaction = transactionBuilderFactory.transaction() // Step 1: When did it occur? .occurredAt(clock.instant()) // Step 2: When should it apply (effective date)? .appliesAt(effectiveDate) // Step 3: What type? .withTypeOf("sale") // Step 4: Executing new transaction or reverting existing? .executing() // or .reverting(transactionId) // Step 5: Add entries .debitFrom(cashAccount, Money.pln(1000)) .creditTo(revenueAccount, Money.pln(1000)) // Step 6: Build (validates balanced) .build();
Key insight: The method sequence enforces business rules:
// Simple value objectpublic record AccountId(String value) { public static AccountId of(String value) { return new AccountId(value); }}// Value object with validationpublic record Money(BigDecimal amount, String currency) { public Money { if (amount == null || currency == null) { throw new IllegalArgumentException("Amount and currency required"); } } public Money add(Money other) { if (!this.currency.equals(other.currency)) { throw new IllegalArgumentException("Currency mismatch"); } return new Money(this.amount.add(other.amount), this.currency); }}
Real example from:quantity/src/main/java/com/softwarearchetypes/quantity/Quantity.java:13
public record Quantity(BigDecimal amount, Unit unit) implements Comparable<Quantity> { public Quantity { checkArgument(amount != null, "Amount cannot be null"); checkArgument(unit != null, "Unit cannot be null"); checkArgument(amount.compareTo(BigDecimal.ZERO) >= 0, "Amount cannot be negative"); } public Quantity add(Quantity other) { checkArgument(this.unit.equals(other.unit), String.format("Cannot add quantities with different units: %s and %s", this.unit, other.unit)); return new Quantity(this.amount.add(other.amount), this.unit); }}
public final class Preconditions { public static void checkArgument(boolean expression, String errorMessage) { if (!expression) { throw new IllegalArgumentException(errorMessage); } } public static void checkState(boolean state, String errorMessage) { if (!state) { throw new IllegalStateException(errorMessage); } } public static void checkNotNull(Object value, String errorMessage) { checkArgument(value != null, errorMessage); }}
When to use records:
Value objects without identity (Money, Quantity, Address)