Advantages of Our Functional Approach

Learn about the advantages of using the functional approach in the application.

Let’s think about the advantages of this approach over our earlier naive version. Before doing that, though, we can discuss some possible improvements as an exercise. For example, our rework contains no tests. Because we focused on creating pure functions and eliminating side effects, adding those would be easy.

Issues with functional approach

There’s some duplication in the code. The higher-level functions in entrypoint.ts and anticorruption.ts are very similar for pets and wild animals. In a larger project, we might create a function that accepts other functions for a more specific behavior, such as creating the right domain object. This is similar to the Template Pattern from OOP, thanks to the versatility of higher-order functions. However, in a small project like this, such optimizations might be premature and work to our disadvantage. What if the access or validation patterns of wild animals and pets are currently similar by accident and will grow apart in the future? If that’s the case and we’ve already moved this seemingly generic functionality to a common section, this function might become a complicated mess with numerous conditionals.

simplistic routing presents another issue, but we’ve already discussed how to fix that. Finally, the injection of an environment variable for the table name is especially glaring since we pointed out its negatives in the original project! In a real application, we’d retrieve these variables using an IO monad, injecting them into functions as parameters. If we had a lot of injection points, we’d turn to the Reader monad.

Advantages

Our code is organized by domain, which can make navigation easier. New features will be mostly contained within a single folder. Want to add a new pet attribute like health? In the current setup, we only need to check out the pets folder and types.ts (we might refactor the application by putting specific types in the right folder). Meanwhile, the common folder has utility functions we hope will be sufficiently generic that we don’t have to explore the actual implementations. The get method retrieves an item while getParallel retrieves items in parallel. We have all the information we need in the name and type.

Benefits of using gateways

We may also have noticed that the basic calls to the database are in the common folder, in a file called gateway, which indicates a focus on communicating with the outside world. Apart from reuse, keeping these calls separate from our domain logic in the other folders also improves testability, except for those basic calls that are little more than wrappers around the AWS SDK, which are probably already tested pretty well by AWS itself. We make our database modification easier by isolating the actual calls to the outside world in a separate file. Do note that if this was our intent, we should’ve created our own parameters instead of littering our database files with AWS types like GetItemInput.

We’ve done our best to limit our code surface, creating only a few entry points to our pets and wild animals. Functions exported by other files in those folders shouldn’t be used anywhere outside the folder. Once we pass beyond our entry points, hooking into functions is now easier. Want to retrieve a pet? Just pass in any object containing an ID and client ID! In a larger application with a more meaningful domain, a large majority of functions would have easier, or at least sensible, types to work with. Here, with our limited domain logic, we have to deal with receiving, validating, and transforming the incoming event.

Our code consists of short, pure functions that are easy to test because they only depend on the given parameters. This makes covering all possible paths through the code easier, as does the limited amount of conditional branching. So, with little effort, we should be able to get high coverage. Also, because larger functionality is built by composing smaller functions, they too are easy to test and understand. Compare this to our earlier situation where we passed along parameters that could be mutated and had several side effects. In a real application, we’d probably have more composition without monads (using fp-ts flow) and more higher-order functions, but the principles remain the same.

This focus on the composition of small, focused functions stands in shrill contrast to the (relatively) large, ill-defined functions in dynamoDb.js. As an example of the former, see getItem, an extremely simple transformation that’s reusable whenever we retrieve an item from the database. This helps keep complexity at bay, though we should take extra care in organizing the codebase, thinking carefully about naming, folder structure, exports, and so on.

Benefits of using hexagonal architecture

Part of our application has now become agnostic about the way we communicate with the outside world. When an event enters our application, we change it into something our domain or code can work with. At the end, we transform our result into something appropriate for REST. This is obviously inspired by hexagonal architecture. We now instead have a functional framework appearing in many layers of our code. In a language like JavaScript, where we lack some essential components for writing functional code, this is unfortunately unavoidable. Perhaps one day the language itself will give us a range of monads to work with.

We’re no longer throwing exceptions that might get caught anywhere (or nowhere). Instead, our flow has become straight and simple. To replace exceptions, we turned to the Either monad, which forced us to handle the issue of invalid values, because our values are now stuck inside the Either box. At some point, we have to get them out!

Error handling is now accomplished in one place, and TypeScript warns us if we forget to handle a type of error. Speaking of type checking, ours is easygoing with decent type inference, yet still delivers value in making our application less error-prone. By setting each property in types.ts to readonly, TypeScript ensures that no objects are mutated.

Performance evaluation

Performance, well managed in the original application, was never our primary concern. Still, webpack’s tree-shaking means we get one small JavaScript file instead of the relatively large original application Zip (4 KB versus 170+KB when unpacked). Though we haven’t made any proper comparisons, the few data points we do have a point to a slightly better average runtime and a very good minimum, just five ms. Because we now use many pure functions, memoization is an easy way to speed things up even further, with little impact on code complexity.

Get hands-on with 1200+ tech skills courses.