How I Built Reliability Using Streams and Either
Single light on track - Photo by Erik Witsoe - https://unsplash.com/photos/XFkWLJQj22A

How I Built Reliability Using Streams and Either

In software engineering, we constantly try to move faster to solve more problems. However, exceptions (especially in languages like Java) slow us down in three ways. First, we have to test for them whenever we leverage outside code (libraries) so we know what to expect and handle those situations gracefully. Second, we investigate further to understand what other cases there are because not all exception cases are documented or checked. If we don't, we make the assumption that these other cases will not happen often, if at all, so we ignore them. Third, each time an unhandled exception is thrown, we spend considerable time investigating the root cause so we can fix the bug.

Recently, I tasked myself with evolving our email reminder system into the most reliable possible. We need our reminders to be sent once and only once, without error. Doing so has been a struggle over the previous months because the system grew in complexity. I've been working with this code for a while, so I know that the game of bug Whack-A-Mole? isn't effective.

Whack-A-Mole

The main issue is if an error occurs when the scheduled job runs, a single email reminder can error without reporting to us so we can track it down and get it sent out later. The process looks loosely like this:

  1. Calculate reminders to be sent, of many different kinds
  2. Reminders assembled, much of the content dynamic in nature
  3. Sends are attempted
  4. Sent reminders are recorded in an audit log

Each step relies on the previous one being successful, so an error in an early step won't necessarily point us quickly to why a problem happened.

A recent episode of Developer Tea explored what exceptions really are and how we perceive them, which made me realize our approach was backwards. We should always expect either a successful state or an exception because these exceptions aren't really exceptional.

Unexceptional exception

The general approach is to handle each possible error or exception state individually, within that logic block, then move on accordingly. Doing that can make code confusing and difficult to maintain. What if we can allow the successful cases to complete, while also handling the errored ones differently?

One thing is for sure, we can't continue with the low possibility of customers receiving the same reminder more than once or missing out on a reminder entirely. Our engagement, customer delight, and company success rely on it.

Java streams, with the help of Either from Atlassian's Fugue library, allow us to create and process the successful while catching and reporting the errored. Creating reminders produces a stream of an Either of the reminder record or a Throwable. At the end of the stream, we can react to the Throwables by reporting them (with the originating exception details because they've flowed down the stream) then continue on and process the successful reminder records. No processing or if/else logic to content with. Here's a abridged example:

final List<Either<Reminder, Throwable>> reminderList = new ArrayList<>();

try {

    List<User> users = userRepository.findByStatus(UNFINISHED);

    reminderList.add(Either.left(Reminder.builder()
            ...
            .build())
    );

} catch (final Throwable t) {
    reminderList.add(Either.right(t));
}
return reminderDataList.stream();

There are no checked exceptions here, but there are a slew of possible database errors which aren't accounted for. This is just one example of how we should expect the unexpected when leveraging code we didn't write...and the code we did.

Before moving on to the next step, we can peek the stream and write the errors to an audit record and/or report them instantly:

.peek(either -> Stream.of(either)
        .filter(Either::isRight)
        .forEach(e -> reminderAuditConsumer.accept(
                ReminderAudit.builder()
                    ...
                    .build()))
);

Since I updated the system using this approach, we've had no unexplainable situations reported by customers, nor have we had to spend significant time investigating reports. The combination of leveraging Either and Java streams has created testable code and a simple method of combining success and failure for processing and reporting.

Jeffrey Fate ??

要查看或添加评论,请登录

?Jeffrey Fate的更多文章

社区洞察

其他会员也浏览了