Better architecture with Railway Oriented Programming

If you’re still throwing exceptions, you’re probably doing it wrong

Eric Silverberg
Geek Culture
6 min readMay 27, 2021

--

Light and Dark. Success and Failure. Functional and Reactive. Photo by Andrew Karn on Unsplash

In order to have an “architecture” to your code, you need a map and a path, a diagram and some arrows. But what, exactly, do we mean by a “path” — what is the nature of the arrows and how do they behave?

Put another way — as we pass data between the layers, what are we passing? In a simple architecture, it is likely that you would be passing a parameters hash down the layers:

And receive a model object up the layers

Which eventually gets mapped to some sort of HTTP response code + a response body.

This strategy works for simple APIs, but what happens when we start to have errors?

Follow the (functional) road!

Handling errors in imperative code

Imagine we have a simple API endpoint, post ‘/user/email’. We want to update the email address of a customer and notify him that it has been changed.

You might imaging the following method in our v1 quick-and-dirty method (implemented here in Sinatra/Ruby):

OK, but how do we handle errors here? We need to check if it is valid:

We also want to check if there is no customer record:

What if the database fails to do its write operation?

And what if our smtp service fails to notify about the change? Maybe we want to log that fact?

With imperative code, error handling is critical, and as APIs scale more errors and edge cases become possible or visible. Error handling in imperative code makes the code much harder to read.

Railway oriented programming

(Note: this section adapted from a presentation by F# for Fun and Profit)

MS actually has a lot of good ideas.

In 2014, a developer from Microsoft coined the term “Railway Oriented Programming”, which derived much inspiration from functional programming methods. He rightly pointed out the importance of “designing the unhappy path” and thinking about functions with more than one output.

When we first model endpoints, we think about them as simple functions with one input and one output:

As the endpoint increases in scope, we may think to break it apart into various steps, layers, components, etc:

But in the real world, we learn that each of these steps has the potential to fail, and we have to provide mechanisms for the interruption of control flow at each of these steps:

The realization that we need functions that return multiple values, either success or error responses, is the foundation of railway oriented programming and the basis of our API design today.

Success and Failure

Errors are Responses too

A failure can occur anywhere in our processing chain, and we needed a streamlined way to exit processing and return a Failure to the client. Or, if everything is good, to return a Success.

The series of classes, or steps used to process the endpoint represents a functional unit, which always results in a Success or Failure, which contain resulting data (for a successful read operation for example) or information about the failure that occurred (such as during validation).

In turn, each step can return a Success or Failure, which the orchestrator (the Controller) uses to stop or continue processing.

Railway Oriented Programming

We visualize the functional processing above as two parallel railroad tracks. For a completely successful request, the train proceeds along the green (Success) track until finished. However, if a failure occurs anywhere along the way, the train switches to the red (Failure) track, and we return the error to the client.

The construction and composition of two-track functions is based on theories in functional programming. There are number of important rules that govern how to build and compose “two-track functions”. We suggest reading through all of the slides at: https://fsharpforfunandprofit.com/rop/

Building two-track in Ruby

Haskell humor. See Stack Overflow.

To build a two-track execution path we use dry-monads. Dry monads defines monads, which is a formal name for a special ruby mixin that defines two new Result types: Success and Failure

Now, any time we might have returned a value, such as an ActiveRecord model object, we now will return a Success(ActiveRecord)

Moreover, whereas we may have called raise SomeError or even halt 4XX, we now will return a Failure(SomeError)

The documentation for dry-monads is described here, but the final form can be seen here:

In this implementation, you can see that find_user now returns a monad, not a user directly. The bind method unwraps success values and makes them available to the blocks. With additional metaprogramming magic thanks to ruby, we can simplify this block even further:

Relationship to Reactive client programming

Thus far we have been talking about server-side architecture, but it is worth noting that the principles we are describing have a direct mapping to client-side architectural patterns as described by Reactive programming.

In Swift, we could rewrite that code block in Combine, which has the concept of a Publisher, which defines Output and Failure types similar to dry-monads above:

In Kotlin, we could rewrite that code block in RxJava using the Single operator, which again defines both value and error outputs:

Updating our W-shaped execution

In a previous blog post we described patterns of web API execution flows. The final pattern we recommended, W-shaped execution, can be updated in the following way:

Two lanes on each path — one for success, one for failure

We know that, in the event we transition to an error state in one of those layers, we will remain on the “Error” track until we ultimately return a value to the user via a POST (or potentially, though not necessarily always, in a Websocket).

Next up

It’s time we start defining some layers! Taking inspiration from Clean architecture and using the principles of V-U-W execution flows and railway oriented programming, we are ready to define the architecture that we use today for our API endpoint design.

More in this series

Other series you might like

Android Activity Lifecycle considered harmful (2021)
Android process death, unexplainable NullPointerExceptions, and the MVVM lifecycle you need right now

Kotlin in Xcode? Swift in Android Studio? (2020)
A series on using Clean + MVVM for consistent architecture on iOS & Android

About the author

Eric Silverberg is CEO of Perry Street Software, publisher of the LGBTQ+ dating apps SCRUFF and Jack’d, with more than 20M members worldwide.

--

--

Eric Silverberg
Geek Culture

CEO, Perry Street Software. Developer. 🏳️‍🌈