Railway-Oriented Programming
Railway-oriented programming is a pattern for modeling the flow of operations that can succeed or fail, visualized as two parallel tracks — the success track and the failure track.
The Pattern
Each operation either continues on the success track or switches to the failure track. Once on the failure track, subsequent operations are bypassed — no exceptions, no hidden control flow.
Basic Pipeline
public Result<ProcessedOrder> ProcessOrder(OrderRequest request){ return ValidateOrderRequest(request) .Bind(ValidateCustomer) .Bind(ValidateInventory) .Bind(CalculatePricing) .Map(CreateOrder);}Advanced Pipeline with Side Effects
public async Task<Result<ProcessedOrder>> ProcessOrderAsync(OrderRequest request){ return await ValidateOrderRequest(request) .Bind(ValidateCustomer) .Bind(ValidateInventory) .Bind(CalculatePricing) .Bind(async order => await SaveOrderAsync(order)) .Bind(async order => await ProcessPaymentAsync(order)) .IfSuccess(async order => await SendConfirmationAsync(order)) .IfFailure(async error => await LogErrorAsync(error));}Starting Pipelines
Use Result.Ensure for cleaner pipeline initialization:
// Instead of:public Result<Unit> Publish(){ if (PublishingStatus == PublishingStatus.Published) return new InvalidOperationError("Already published");
return ValidateCostComponents() .Bind(ValidateTimingComponents) .IfSuccess(() => PublishingStatus = PublishingStatus.Published);}
// Prefer:public Result<Unit> Publish() => Result .Ensure(PublishingStatus != PublishingStatus.Published, () => new InvalidOperationError("Already published")) .Bind(ValidateCostComponents) .Bind(ValidateTimingComponents) .IfSuccess(() => PublishingStatus = PublishingStatus.Published);Error Recovery
var config = LoadConfigFromFile() .RecoverWith(() => LoadConfigFromEnvironment()) .RecoverWith(GetDefaultConfig());Combining Maybe and Result
public Result<UserProfile> GetUserProfile(int userId){ return FindUser(userId) // Maybe<User> .MapToResult(() => new NotFoundError("User not found")) .Bind(user => LoadUserProfile(user)) // Result<UserProfile> .Map(profile => EnrichProfile(profile));}Benefits
- Explicit error handling - No hidden exceptions
- Composable operations - Easy to add/remove steps
- Readable flow - Clear success/failure paths
- Testable - Each step can be tested in isolation
See Also
Result\ Monad Core Result monad operations and patterns
Async Programming Use railway patterns with async/await