mirror of
https://github.com/s-frick/effigenix.git
synced 2026-03-28 13:49:36 +01:00
docs: and skills
This commit is contained in:
parent
e4f0665086
commit
ccd4ee534a
25 changed files with 10412 additions and 0 deletions
781
bin/.claude/skills/ddd-model/languages/java/error-handling.md
Normal file
781
bin/.claude/skills/ddd-model/languages/java/error-handling.md
Normal file
|
|
@ -0,0 +1,781 @@
|
|||
# Error Handling in Java 21+
|
||||
|
||||
This guide covers error handling in Java projects following DDD principles, with emphasis on Result types and sealed interfaces.
|
||||
|
||||
## Result<E, T> Interface
|
||||
|
||||
The fundamental pattern for error handling without exceptions:
|
||||
|
||||
### Definition
|
||||
|
||||
```java
|
||||
package com.example.shared.result;
|
||||
|
||||
/**
|
||||
* Result represents either an error (left) or a value (right).
|
||||
* Inspired by Either/Result from functional languages.
|
||||
*
|
||||
* @param <E> Error type (must be sealed interface or final class)
|
||||
* @param <T> Success value type
|
||||
*/
|
||||
public sealed interface Result<E, T> permits Failure, Success {
|
||||
/**
|
||||
* Applies function if result is success.
|
||||
*/
|
||||
<U> Result<E, U> map(java.util.function.Function<T, U> fn);
|
||||
|
||||
/**
|
||||
* Chains Result-returning operations.
|
||||
*/
|
||||
<U> Result<E, U> flatMap(java.util.function.Function<T, Result<E, U>> fn);
|
||||
|
||||
/**
|
||||
* Applies error function if result is failure.
|
||||
*/
|
||||
Result<E, T> mapError(java.util.function.Function<E, E> fn);
|
||||
|
||||
/**
|
||||
* Pattern matching on Result.
|
||||
*/
|
||||
<U> U match(
|
||||
java.util.function.Function<E, U> onError,
|
||||
java.util.function.Function<T, U> onSuccess
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns value or throws if error.
|
||||
*/
|
||||
T orElseThrow(java.util.function.Function<E, ? extends RuntimeException> exceptionFn);
|
||||
|
||||
/**
|
||||
* Returns value or default if error.
|
||||
*/
|
||||
T orElse(T defaultValue);
|
||||
|
||||
/**
|
||||
* Success result (right side).
|
||||
*/
|
||||
static <E, T> Result<E, T> success(T value) {
|
||||
return new Success<>(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Failure result (left side).
|
||||
*/
|
||||
static <E, T> Result<E, T> failure(E error) {
|
||||
return new Failure<>(error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for success().
|
||||
*/
|
||||
static <E, T> Result<E, T> ok(T value) {
|
||||
return success(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for failure().
|
||||
*/
|
||||
static <E, T> Result<E, T> err(E error) {
|
||||
return failure(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Success case - carries the successful value.
|
||||
*/
|
||||
final class Success<E, T> implements Result<E, T> {
|
||||
private final T value;
|
||||
|
||||
Success(T value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <U> Result<E, U> map(java.util.function.Function<T, U> fn) {
|
||||
return new Success<>(fn.apply(value));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <U> Result<E, U> flatMap(java.util.function.Function<T, Result<E, U>> fn) {
|
||||
return fn.apply(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<E, T> mapError(java.util.function.Function<E, E> fn) {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <U> U match(
|
||||
java.util.function.Function<E, U> onError,
|
||||
java.util.function.Function<T, U> onSuccess
|
||||
) {
|
||||
return onSuccess.apply(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public T orElseThrow(java.util.function.Function<E, ? extends RuntimeException> exceptionFn) {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public T orElse(T defaultValue) {
|
||||
return value;
|
||||
}
|
||||
|
||||
public T value() {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Success(" + value + ")";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Failure case - carries the error.
|
||||
*/
|
||||
final class Failure<E, T> implements Result<E, T> {
|
||||
private final E error;
|
||||
|
||||
Failure(E error) {
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <U> Result<E, U> map(java.util.function.Function<T, U> fn) {
|
||||
return new Failure<>(error);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <U> Result<E, U> flatMap(java.util.function.Function<T, Result<E, U>> fn) {
|
||||
return new Failure<>(error);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<E, T> mapError(java.util.function.Function<E, E> fn) {
|
||||
return new Failure<>(fn.apply(error));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <U> U match(
|
||||
java.util.function.Function<E, U> onError,
|
||||
java.util.function.Function<T, U> onSuccess
|
||||
) {
|
||||
return onError.apply(error);
|
||||
}
|
||||
|
||||
@Override
|
||||
public T orElseThrow(java.util.function.Function<E, ? extends RuntimeException> exceptionFn) {
|
||||
throw exceptionFn.apply(error);
|
||||
}
|
||||
|
||||
@Override
|
||||
public T orElse(T defaultValue) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public E error() {
|
||||
return error;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Failure(" + error + ")";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Static Imports for Readability
|
||||
|
||||
Define static import helpers in your domain:
|
||||
|
||||
```java
|
||||
// In com.example.shared.result.Results
|
||||
public class Results {
|
||||
private Results() {}
|
||||
|
||||
public static <E, T> Result<E, T> ok(T value) {
|
||||
return Result.success(value);
|
||||
}
|
||||
|
||||
public static <E, T> Result<E, T> fail(E error) {
|
||||
return Result.failure(error);
|
||||
}
|
||||
}
|
||||
|
||||
// Usage with static import
|
||||
import static com.example.shared.result.Results.*;
|
||||
|
||||
Result<AccountError, Account> result = ok(account);
|
||||
Result<AccountError, Void> error = fail(new InsufficientFundsError());
|
||||
```
|
||||
|
||||
## Sealed Interfaces for Domain Errors
|
||||
|
||||
Layer-specific errors as sealed interfaces:
|
||||
|
||||
### Domain Layer Errors
|
||||
|
||||
```java
|
||||
package com.example.domain.account;
|
||||
|
||||
/**
|
||||
* Account domain errors - business rule violations.
|
||||
*/
|
||||
public sealed interface AccountError permits
|
||||
InsufficientFundsError,
|
||||
AccountClosedError,
|
||||
InvalidAmountError,
|
||||
AccountNotFoundError {
|
||||
|
||||
String message();
|
||||
}
|
||||
|
||||
public record InsufficientFundsError(
|
||||
Money required,
|
||||
Money available
|
||||
) implements AccountError {
|
||||
@Override
|
||||
public String message() {
|
||||
return String.format(
|
||||
"Insufficient funds: required %s, available %s",
|
||||
required, available
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public record AccountClosedError(
|
||||
AccountId accountId
|
||||
) implements AccountError {
|
||||
@Override
|
||||
public String message() {
|
||||
return "Account is closed: " + accountId;
|
||||
}
|
||||
}
|
||||
|
||||
public record InvalidAmountError(
|
||||
Money amount,
|
||||
String reason
|
||||
) implements AccountError {
|
||||
@Override
|
||||
public String message() {
|
||||
return String.format("Invalid amount %s: %s", amount, reason);
|
||||
}
|
||||
}
|
||||
|
||||
public record AccountNotFoundError(
|
||||
AccountId accountId
|
||||
) implements AccountError {
|
||||
@Override
|
||||
public String message() {
|
||||
return "Account not found: " + accountId;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Application Layer Errors
|
||||
|
||||
```java
|
||||
package com.example.application.account;
|
||||
|
||||
/**
|
||||
* Application layer errors - use case failures.
|
||||
* Maps domain errors to application context.
|
||||
*/
|
||||
public sealed interface WithdrawError permits
|
||||
InsufficientFundsError,
|
||||
AccountLockedError,
|
||||
RepositoryError,
|
||||
ConcurrencyError {
|
||||
|
||||
String message();
|
||||
}
|
||||
|
||||
public record InsufficientFundsError(
|
||||
String accountId,
|
||||
java.math.BigDecimal required,
|
||||
java.math.BigDecimal available
|
||||
) implements WithdrawError {
|
||||
@Override
|
||||
public String message() {
|
||||
return String.format(
|
||||
"Cannot withdraw: insufficient funds in account %s",
|
||||
accountId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public record AccountLockedError(
|
||||
String accountId,
|
||||
String reason
|
||||
) implements WithdrawError {
|
||||
@Override
|
||||
public String message() {
|
||||
return String.format("Account %s is locked: %s", accountId, reason);
|
||||
}
|
||||
}
|
||||
|
||||
public record RepositoryError(
|
||||
String cause
|
||||
) implements WithdrawError {
|
||||
@Override
|
||||
public String message() {
|
||||
return "Repository error: " + cause;
|
||||
}
|
||||
}
|
||||
|
||||
public record ConcurrencyError(
|
||||
String accountId
|
||||
) implements WithdrawError {
|
||||
@Override
|
||||
public String message() {
|
||||
return "Account was modified concurrently: " + accountId;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Infrastructure Layer Errors
|
||||
|
||||
```java
|
||||
package com.example.infrastructure.persistence;
|
||||
|
||||
/**
|
||||
* Infrastructure errors - technical failures.
|
||||
* Never leak to domain/application layer.
|
||||
*/
|
||||
public sealed interface PersistenceError permits
|
||||
ConnectionError,
|
||||
QueryError,
|
||||
TransactionError,
|
||||
DeserializationError {
|
||||
|
||||
String message();
|
||||
Throwable cause();
|
||||
}
|
||||
|
||||
public record ConnectionError(
|
||||
String reason,
|
||||
Throwable exception
|
||||
) implements PersistenceError {
|
||||
@Override
|
||||
public String message() {
|
||||
return "Database connection failed: " + reason;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Throwable cause() {
|
||||
return exception;
|
||||
}
|
||||
}
|
||||
|
||||
public record QueryError(
|
||||
String sql,
|
||||
Throwable exception
|
||||
) implements PersistenceError {
|
||||
@Override
|
||||
public String message() {
|
||||
return "Query execution failed: " + sql;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Throwable cause() {
|
||||
return exception;
|
||||
}
|
||||
}
|
||||
|
||||
public record TransactionError(
|
||||
String reason,
|
||||
Throwable exception
|
||||
) implements PersistenceError {
|
||||
@Override
|
||||
public String message() {
|
||||
return "Transaction failed: " + reason;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Throwable cause() {
|
||||
return exception;
|
||||
}
|
||||
}
|
||||
|
||||
public record DeserializationError(
|
||||
String reason,
|
||||
Throwable exception
|
||||
) implements PersistenceError {
|
||||
@Override
|
||||
public String message() {
|
||||
return "Deserialization failed: " + reason;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Throwable cause() {
|
||||
return exception;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Pattern Matching Examples
|
||||
|
||||
### Switch Expression with Pattern Matching
|
||||
|
||||
```java
|
||||
public static void handleWithdrawalResult(Result<WithdrawError, Account> result) {
|
||||
switch (result) {
|
||||
case Success(Account account) -> {
|
||||
System.out.println("Withdrawal successful, new balance: " + account.balance());
|
||||
}
|
||||
case Failure(InsufficientFundsError error) -> {
|
||||
System.err.println(error.message());
|
||||
}
|
||||
case Failure(AccountLockedError error) -> {
|
||||
System.err.println("Please contact support: " + error.reason());
|
||||
}
|
||||
case Failure(WithdrawError error) -> {
|
||||
System.err.println("Unexpected error: " + error.message());
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Map and FlatMap Chaining
|
||||
|
||||
```java
|
||||
public Result<TransferError, TransferRecord> transfer(
|
||||
AccountId fromId,
|
||||
AccountId toId,
|
||||
Money amount
|
||||
) {
|
||||
return accountRepository.findById(fromId)
|
||||
.mapError(e -> new SourceAccountNotFound(fromId))
|
||||
.flatMap(fromAccount ->
|
||||
accountRepository.findById(toId)
|
||||
.mapError(e -> new DestinationAccountNotFound(toId))
|
||||
.flatMap(toAccount -> {
|
||||
Result<?, Void> withdrawn = fromAccount.withdraw(amount);
|
||||
if (withdrawn instanceof Failure failure) {
|
||||
return Result.failure(new WithdrawalFailed(failure.error().message()));
|
||||
}
|
||||
|
||||
Result<?, Void> deposited = toAccount.deposit(amount);
|
||||
if (deposited instanceof Failure failure) {
|
||||
return Result.failure(new DepositFailed(failure.error().message()));
|
||||
}
|
||||
|
||||
return Result.success(new TransferRecord(fromId, toId, amount));
|
||||
})
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Match Expression for Conditional Logic
|
||||
|
||||
```java
|
||||
public String formatWithdrawalStatus(Result<WithdrawError, Account> result) {
|
||||
return result.match(
|
||||
error -> "Failed: " + error.message(),
|
||||
account -> "Success: New balance is " + account.balance().formatted()
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Layer-Specific Error Transformation
|
||||
|
||||
### Domain to Application Boundary
|
||||
|
||||
```java
|
||||
package com.example.application.account;
|
||||
|
||||
import com.example.domain.account.AccountError;
|
||||
import com.example.infrastructure.persistence.PersistenceError;
|
||||
|
||||
public class WithdrawService {
|
||||
private final AccountRepository repository;
|
||||
|
||||
/**
|
||||
* Maps domain errors to application errors.
|
||||
* Infrastructure errors caught at boundary.
|
||||
*/
|
||||
public Result<WithdrawError, Account> withdraw(
|
||||
AccountId accountId,
|
||||
Money amount
|
||||
) {
|
||||
try {
|
||||
// Repository might fail with infrastructure errors
|
||||
return repository.findById(accountId)
|
||||
.mapError(error -> mapDomainError(error))
|
||||
.flatMap(account ->
|
||||
account.withdraw(amount)
|
||||
.mapError(error -> mapDomainError(error))
|
||||
);
|
||||
} catch (Exception e) {
|
||||
// Infrastructure exception caught at boundary
|
||||
logger.error("Unexpected infrastructure error during withdrawal", e);
|
||||
return Result.failure(new RepositoryError(e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
private WithdrawError mapDomainError(AccountError error) {
|
||||
return switch (error) {
|
||||
case InsufficientFundsError e ->
|
||||
new InsufficientFundsError(
|
||||
e.required().toString(),
|
||||
e.available().toString()
|
||||
);
|
||||
case AccountClosedError e ->
|
||||
new AccountLockedError(e.accountId().value(), "Account closed");
|
||||
case InvalidAmountError e ->
|
||||
new RepositoryError(e.reason());
|
||||
case AccountNotFoundError e ->
|
||||
new RepositoryError("Account not found: " + e.accountId());
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Application to Infrastructure Boundary
|
||||
|
||||
```java
|
||||
package com.example.infrastructure.persistence;
|
||||
|
||||
import com.example.domain.account.Account;
|
||||
import java.sql.Connection;
|
||||
|
||||
public class JdbcAccountRepository implements AccountRepository {
|
||||
|
||||
@Override
|
||||
public Result<RepositoryError, Account> findById(AccountId id) {
|
||||
Connection conn = null;
|
||||
try {
|
||||
conn = dataSource.getConnection();
|
||||
Account account = executeQuery(conn, id);
|
||||
return Result.success(account);
|
||||
} catch (SQLException e) {
|
||||
// Log original exception
|
||||
logger.error("SQL error querying account: " + id, e);
|
||||
return Result.failure(new RepositoryError(
|
||||
"Database query failed: " + e.getMessage()
|
||||
));
|
||||
} catch (Exception e) {
|
||||
logger.error("Unexpected error deserializing account: " + id, e);
|
||||
return Result.failure(new RepositoryError(
|
||||
"Deserialization failed: " + e.getMessage()
|
||||
));
|
||||
} finally {
|
||||
if (conn != null) {
|
||||
try { conn.close(); } catch (SQLException e) {
|
||||
logger.warn("Error closing connection", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Account executeQuery(Connection conn, AccountId id) throws SQLException {
|
||||
String sql = "SELECT * FROM accounts WHERE id = ?";
|
||||
try (var stmt = conn.prepareStatement(sql)) {
|
||||
stmt.setString(1, id.value());
|
||||
var rs = stmt.executeQuery();
|
||||
if (!rs.next()) {
|
||||
throw new SQLException("Account not found: " + id);
|
||||
}
|
||||
return mapRowToAccount(rs);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Exception Boundaries
|
||||
|
||||
### Infrastructure Layer Only Catches External Exceptions
|
||||
|
||||
```java
|
||||
package com.example.infrastructure.payment;
|
||||
|
||||
import org.stripe.exception.StripeException;
|
||||
|
||||
public class StripePaymentGateway implements PaymentGateway {
|
||||
|
||||
@Override
|
||||
public Result<PaymentError, PaymentConfirmation> charge(
|
||||
String token,
|
||||
Money amount
|
||||
) {
|
||||
try {
|
||||
// Stripe API call
|
||||
var charge = Stripe.Charges.create(
|
||||
token, amount.centAmount()
|
||||
);
|
||||
return Result.success(new PaymentConfirmation(charge.id));
|
||||
|
||||
} catch (StripeException e) {
|
||||
// Log original exception
|
||||
logger.error("Stripe API error charging payment", e);
|
||||
|
||||
// Transform to domain error
|
||||
PaymentError error = switch (e.getCode()) {
|
||||
case "card_declined" ->
|
||||
new CardDeclinedError(e.getMessage());
|
||||
case "rate_limit" ->
|
||||
new PaymentUnavailableError("Rate limited by Stripe");
|
||||
default ->
|
||||
new PaymentFailedError(e.getMessage());
|
||||
};
|
||||
|
||||
return Result.failure(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Domain/Application Layers Never Catch Exceptions
|
||||
|
||||
```java
|
||||
// ❌ DON'T DO THIS in domain/application
|
||||
public class Order {
|
||||
public Result<OrderError, Void> place() {
|
||||
try { // ❌ No try/catch in domain
|
||||
validate();
|
||||
return Result.success(null);
|
||||
} catch (Exception e) { // ❌ Wrong
|
||||
return Result.failure(new OrderError(...));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ DO THIS instead
|
||||
public class Order {
|
||||
public Result<OrderError, Void> place() {
|
||||
if (!isValid()) { // ✅ Guard clauses instead
|
||||
return Result.failure(new InvalidOrderError(...));
|
||||
}
|
||||
return Result.success(null);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Logging Strategy
|
||||
|
||||
### ERROR Level - Infrastructure Failures
|
||||
|
||||
```java
|
||||
// Database down, network error, external service error
|
||||
logger.error("Database connection failed", e); // Log original exception
|
||||
logger.error("Stripe API error during payment", stripeException);
|
||||
logger.error("Kafka publishing failed for event: " + eventId, kafkaException);
|
||||
```
|
||||
|
||||
### WARN Level - Business Rule Violations
|
||||
|
||||
```java
|
||||
// Insufficient funds, account closed, invalid state
|
||||
logger.warn("Withdrawal rejected: insufficient funds in account " + accountId);
|
||||
logger.warn("Order cannot be placed with empty line items");
|
||||
logger.warn("Transfer cancelled: destination account closed");
|
||||
```
|
||||
|
||||
### INFO Level - Normal Operations
|
||||
|
||||
```java
|
||||
// Expected business events
|
||||
logger.info("Account created: " + accountId);
|
||||
logger.info("Transfer completed: " + transferId + " from " + fromId + " to " + toId);
|
||||
logger.info("Order placed: " + orderId + " for customer " + customerId);
|
||||
```
|
||||
|
||||
### DEBUG Level - Detailed Flow
|
||||
|
||||
```java
|
||||
// Control flow details (not shown in production)
|
||||
logger.debug("Starting account balance calculation for: " + accountId);
|
||||
logger.debug("Processing " + lineItems.size() + " line items");
|
||||
```
|
||||
|
||||
### Logging at Layer Boundaries
|
||||
|
||||
```java
|
||||
public class WithdrawService {
|
||||
public Result<WithdrawError, Account> withdraw(
|
||||
AccountId accountId,
|
||||
Money amount
|
||||
) {
|
||||
return repository.findById(accountId)
|
||||
.mapError(domainError -> {
|
||||
// Log domain error at application level
|
||||
logger.warn("Account not found during withdrawal: " + accountId);
|
||||
return mapToApplicationError(domainError);
|
||||
})
|
||||
.flatMap(account ->
|
||||
account.withdraw(amount)
|
||||
.mapError(domainError -> {
|
||||
if (domainError instanceof InsufficientFundsError e) {
|
||||
logger.warn("Insufficient funds: required " + e.required() +
|
||||
", available " + e.available());
|
||||
}
|
||||
return mapToApplicationError(domainError);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public class JdbcAccountRepository implements AccountRepository {
|
||||
@Override
|
||||
public Result<RepositoryError, Account> findById(AccountId id) {
|
||||
try {
|
||||
return executeQuery(id);
|
||||
} catch (SQLException e) {
|
||||
// Log technical error at infrastructure level
|
||||
logger.error("SQL error querying account " + id, e);
|
||||
return Result.failure(new RepositoryError("Query failed: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use sealed interfaces** for all domain/application errors
|
||||
2. **Use records** for error data classes (immutable, concise)
|
||||
3. **Add `message()` method** to every error type
|
||||
4. **Transform errors at boundaries** (domain → application → infrastructure)
|
||||
5. **Log original exceptions** before transformation
|
||||
6. **Use static imports** for Result.success/failure
|
||||
7. **Pattern match** instead of instanceof checks
|
||||
8. **Avoid null returns** - use Result.failure instead
|
||||
9. **Document invariants** that cause errors
|
||||
10. **Never expose infrastructure errors** to domain/application
|
||||
|
||||
## Testing Error Cases
|
||||
|
||||
```java
|
||||
public class WithdrawalTest {
|
||||
@Test
|
||||
void shouldRejectWithdrawalExceedingBalance() {
|
||||
var account = Account.create(
|
||||
accountId,
|
||||
Money.usd(100)
|
||||
).orElseThrow();
|
||||
|
||||
Result<WithdrawError, Account> result = account.withdraw(Money.usd(150));
|
||||
|
||||
assertThat(result).satisfies(r -> {
|
||||
assertThat(r).isInstanceOf(Failure.class);
|
||||
if (r instanceof Failure f) {
|
||||
assertThat(f.error()).isInstanceOf(InsufficientFundsError.class);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMapDomainErrorToApplicationError() {
|
||||
var domainError = new AccountNotFoundError(accountId);
|
||||
var applicationError = service.mapDomainError(domainError);
|
||||
|
||||
assertThat(applicationError)
|
||||
.isInstanceOf(RepositoryError.class);
|
||||
assertThat(applicationError.message())
|
||||
.contains("Account not found");
|
||||
}
|
||||
}
|
||||
```
|
||||
Loading…
Add table
Add a link
Reference in a new issue