Hi there! I’m Pedro Moreira, CTO @ Landing.jobs
When I joined this crazy awesome team almost two years ago we were two moonlighting developers on a deadline. There was a rough prototype, a lot of ideas, and few months to build something.
In an early-stage startup, “time-to-feedback” is one of the most important variables a developer needs to optimise for. Feedback from your early adopters is a priceless commodity. It is the fuel that drives further iterations and improvements to your product.
So you start by keeping features as simple as possible, stay as lean as possible, and avoid over-engineering as much as you can. But sometimes compromises will have to be made.
Sometimes the right decision is to take on some technical debt.
As the product grows, that debt needs to be managed and gradually paid. One part of our product that was in need of some attention was our notifications system.
Until recently, we had two different kinds of notifications:
- In-app notifications, built on top of PublicActivity
- Email notifications, which were simply async
ActionMailer
emails, or in a few cases, Sidekiq workers.
These had been built at different times, and we had never wrapped these two distinct concepts into a common interface. As an example, this is what happened when a user recommended a friend:
- Create a recommendation
- Update the associated recommendation request
- Create an in-app notification
- Queue a job to send email notifications
- Log the event to Intercom
“If you are not embarrassed by the first version of your product, you’ve launched too late.” (Reid Hoffman) — I think this may also apply your code.
This pattern was common throughout our code. When a candidate applied to an offer, received a recommendation or a referral, or a company posted a new offer, for example, we needed to create the notifications, send emails and track the event. Some of these controller actions were getting a bit out of hand, so it was clearly time to pay back some debt.
A few months ago we began using Interactor to put our controllers and models on a diet. We’ve been gradually moving our business logic into interactors, decoupling several models and eliminating AR callbacks (and making our tests much simpler in the process), but the notifications system was essentially unchanged during this process.
As part of the Marty McFly release we wanted to introduce digest emails for notifications, and let our users choose when and how to receive them. This would require a lot of changes to the existing code, so we decided it was time to go back to the proverbial drawing board and re-think how we handled and delivered notifications.
Instead of separate in-app and email notifications, we now have a generic Notification
model, that can be rendered as an in-app notification, a partial in a digest email, or a full-length email. But more importantly, the new back-end for notifications was built on a pub-sub based system, using Wisper.
Combining Interactor and Wisper (plus Wisper::Sidekiq), we ended up with much simpler controller actions: The controller simply needs to invoke the interactor, and then redirect or render based on the resulting success or failure.
On success, the interactor broadcasts a Wisper event, which is handled by asynchronous listeners. Some listeners handle notifications, some handle the Intercom logging, and so forth.
Since we had already been migrating parts of our business logic into interactor objects, part of the work was already done. Wiring up Interactor and Wisper was pretty straightforward. Async listeners receive the events and take care of how notifications are delivered.
This event-driven system helped us decouple all the logic behind the notifications delivery from the various actions that trigger the creation of a notification. But now we’re also using listeners to handle Intercom event logging, or to trigger our matching algorithm when you update your profile.
There’s still some work ahead, but we’ve taken some big steps towards a cleaner, modular architecture.
Pedro Moreira
CTO
Landing.jobs
Aim for more.
PS: More about what we shipped with the McFly release here.
0 Comments