Every application deals with functionality that is not part of its core scope. Todo list applications need to send reminders, taxi apps need to mail receipts and review apps need to update average scores. None of this belongs to the core functionality of the application but is needed to improve its user experience. So how do you deal with this functionality creep?
At Sjauf we strive for single-functionality resolver functions. This means every request only does one thing, keeping our resolvers clean and simple. Whenever our application wants to complete another action, it needs to send another request. For most functionality, this makes a lot of sense. You don't want your requests to have unexpected consequences. Unfortunately, sometimes single functionality is not enough. We'll take our trip booking functionality as an example.
When a user books a trip we have to take these steps:
- Validate user input
- Calculate a price
- Save the trip in our database
- Send a confirmation email to the user
- Send notifications to our drivers
- Send a chat message to our user
- Send an email to our planning department
While steps 1 through 3 are all true to the core responsibility of this request, anything else can be considered extra and should -- in our design philosophy -- be moved out of this function/resolver. So where do these functions go and how do we call them?
There are a few solutions we can come up with.
Screw our philosophy, do it all in one place!
If we ignore our philosophy, we can simply put this extra functionality in the resolver function we wrote. With proper separation of functionality in the rest of the app, it's easy to create a function for every step and simply call these functions on a background thread, to make sure your resolver isn't unnecessarily delayed.
Unfortunately, I don't believe in ignoring a philosophy I've worked hard to develop and implement. Sure, it might seem easy to break your own rules for once but it sets a precedent, resulting in more rule breaking. That's a slippery slope so proceed with caution!
Using database listeners, you can perform functionality when a change happens in your database. For the booking process, we'll listen to new trips being created. An advantage of this is that you can separate this solution entirely from all other functionality. You can even run it in a completely standalone application, as long as it connects to your database. You also have access to the entire row you just created/updated right when it happens. As a final cherry on top of the already pretty decent cake, these listeners will get triggered even when you update the database manually or through an admin panel or other application.
There is a huge downside to this way of doing things. Because database listeners always trigger, you have no control over what might happen. Uncontrollable and unforeseen consequences everywhere! Also, because your code would most of the time not be in the same file as your core functionality, it would be hard to understand the flow of your data.
Hooking is the act of intercepting a function in order to change its functionality. For our purposes, we would hook into the booking function to perform our extra functionality after the trip has been booked. You would most likely keep these hooks in the same file for clarity. You keep all functionality where it's supposed to be without losing sight of where your code lives.
Again, there's a downside. Not every language has hook functionality, meaning you might have to implement a version of this yourself. Also, because you're separating functionality, you will need to either document your after_function callbacks perfectly or create a pattern that doesn't require documentation. I'll leave that to you.
Your solution to this problem will depend on your particular stack and ability to document. I would always advise against just stuffing all functionality in one function and calling it a day. It's quick but it also creates functions that do more than one thing, which is a bad idea. Database listeners might lead to unexpected consequences but at least the code is separated. Function hooks seem to be the best way to go here. They allow you to separate your code while keeping some form of clarity. Unfortunately, this pattern requires documentation and a decent amount of discipline which can be tricky.
In the end, this is a design choice and it's up to you to solve. What solution do you prefer?