Adding Functional Flavor to Domain-Driven Design with Either
Incorporating functional programming concepts into domain-driven design (DDD) can add clarity and expressiveness to our code. When implementing business logic methods, we often face multiple potential outcomes. Handling these outcomes in a readable, maintainable way becomes crucial as business requirements evolve. This is where the Either<T>
class shines, giving us an elegant solution to capture success and error states in a way that aligns with functional programming principles.
User Story Example: Creating a Profile
Let’s consider the following user story:
User Story:
As a user, I need to be able to create my profile in the application. I will provide certain information from a recent billing statement, and the system should identify me and set up my profile accordingly. Wholesale and cash customers are not allowed to register, nor are users without permission to add an account.
This requires validating user information, which could lead to a variety of outcomes:
UserNotFound
AccountIsAlreadyAssociated
AccountIsWholeSaleError
CashCustomerError
NoPermissionToAddAccountError
Previous Approach: Using Standard Error Types
In previous implementations, I used a standard error type in the method signature:
public AddAccountBusinessFlow(data): Promise<bool | Error>
This approach returned either a boolean on success or an error on failure. Calling code could then check against various errors:
if (error == UserNotFound) return response.status(404);
else if (error == AccountIsAlreadyAssociated || error == AccountIsWholeSaleError) return response.status(400);
While functional, this quickly becomes unwieldy as conditions grow. A more readable and expressive method signature would improve maintainability, self-documenting the method's possible outcomes.
Functional Solution: The Either Type
Inspired by Rust’s Result
monad and Either
type, I explored using TypeScript’s Either
to handle these multiple outcomes in a more expressive manner. By using an Either
type, we can clarify the semantics of our method signatures and improve readability in a domain-driven context.
Here's how an Either
implementation can make the method more expressive:
Either<
| UserNotFound
| AccountIsAlreadyAssociated
| CashCustomerError
| NoPermissionToAddAccountError,
AddAccountSuccess>
With this Either
type, all possible error conditions are listed on the left, and the success state (AddAccountSuccess
) is on the right. This pattern makes it clear that the function can either succeed with a valid outcome or return one of the specific error states. The method signature itself now serves as documentation, making it easier for anyone reading the code to understand potential outcomes.
Either Implementation in TypeScript
Here’s the Either
implementation I used, inspired by an excellent article on expressive error handling in TypeScript here:
export type Either<L, A> = Left<L, A> | Right<L, A>;
export class Left<L, A> {
readonly value: L;
constructor(value: L) {
this.value = value;
}
isLeft(): this is Left<L, A> {
return true;
}
isRight(): this is Right<L, A> {
return false;
}
}
export class Right<L, A> {
readonly value: A;
constructor(value: A) {
this.value = value;
}
isLeft(): this is Left<L, A> {
return false;
}
isRight(): this is Right<L, A> {
return true;
}
}
export const left = <L, A>(l: L): Either<L, A> => {
return new Left(l);
};
export const right = <L, A>(a: A): Either<L, A> => {
return new Right(l);
};
This Either
type allows us to encapsulate both success and error states, enforcing at compile time that the caller must handle both cases.
Using Either
in AddAccountBusinessFlow
With the Either
pattern, our AddAccountBusinessFlow
can now return specific outcomes in a functional style, improving clarity and reliability. Here’s how we can use it:
public AddAccountBusinessFlow(data): Promise<Either<
| UserNotFound
| AccountIsAlreadyAssociated
| CashCustomerError
| NoPermissionToAddAccountError,
AddAccountSuccess>> {
// Business logic here...
if (!userExists) return left(new UserNotFound());
if (accountIsWholesale) return left(new AccountIsWholeSaleError());
if (!userHasPermission) return left(new NoPermissionToAddAccountError());
return right(new AddAccountSuccess());
}
Benefits of the Either Approach in Domain-Driven Design
Semantic Clarity: By naming each error explicitly in the method signature, we create a more readable and self-documenting method. This is invaluable as the codebase grows.
Compile-Time Safety: Using
Either
enforces that both success and error states are considered, reducing the risk of unhandled errors and making refactoring safer.Functional Flow:
Either
facilitates functional-style error handling, reducing branching code and making error handling more declarative.Enhanced Testability: Testing individual outcomes is straightforward. The function’s output type captures all possible paths, making it easy to verify each scenario.
Conclusion
The Either
type brings the benefits of functional programming to TypeScript, making complex business logic more manageable and expressive. By applying this to domain-driven design, we get a cleaner, more readable codebase that is easier to maintain and test.
Top comments (0)