Fountain’s high-volume hiring platform helps organizations find and hire the right people, faster. One early feature in Fountain was a calendar view that listed a manager’s upcoming interviews, let applicants schedule interviews in available slots, and provided a summary of upcoming interviewees.
We’ve learned a key fact since our beginnings - managers don’t need a second calendar. What they need is one integrated view.
Our internal model of a calendar historically revolved around availability: when an applicant and recruiter could potentially meet. Availability was created manually by the user through an ‘add availability modal’ that put many options in the hands of the interviewer. One of the main pain points with this model was the lack of synchronization back to the interviewer’s primary calendar, complicating rescheduling and causing missed bookings. This functionality also required recruiters to add more slots if applicants selected all that were available, thus limiting connection potential of applicants and recruiters.
These interview slots also required considerable manual effort to duplicate instructions and locations when creating new openings. This raised the question: could we provide a template for these events and apply it on every slot.
We knew we had to represent when an interview could occur, but shuffling events off calendars seemed brittle. How often should we poll for changes? What do we gain by consuming a webhook? Adding more data to Fountain might make us better at avoiding conflicts, but that solution felt heavy.
Grand ideas of improved functionality
The more user research we did, the clearer it became—less calendar is more. As Richard Gabriel would say, Worse is Better. The previous upfront design was too complicated.
We were left with a decision between optimizing our existing functionality to get something closer to what our customers were clamoring for or start completely from scratch. We chose to make something completely different.
The biggest challenge to overhauling any product is making the new implementation work side by side with the last one. With a short timeline to prove the new approach, we needed small increments and minimal breaking changes, plus a cadence of demoing and refining daily.
We turned to an existing service partner, cronofy, to help reduce development overhead. With Cronofy (and OAuth2) abstracting calendar access, we got a convenient abstraction over Google calendars and any of the Microsoft offerings, plus UI elements that have allowed us to remove most of the design from the initial scope.
We also added a session template to allow users to quickly repeat form information, and were able to then generate multiple time slots for the same opening quickly.
Cronofy had a much different implementation for bookable events than historically existed in Fountain.
With more than half our bookings happening through these capacity-managed events, this wasn’t functionality we could eliminate. We reviewed support in the SDK for the feature and found none. We architected a process flow for generating these events and it looked a lot like the design we were walking away from—when applicants booked these group events, separate events for every applicant showed up on the recruiter’s calendar!
In the end, we chose instead to manage event transparency. We designated interviews with remaining capacity as either free or transparent. Leveraging our existing capacity management, we now update events as they receive bookings. In our upgrade we also consolidated the duplicated calendar events and invited the applicants into a unified event.
Calendars typically keep track of the impact an event has on your free/busy information. If your coworker is out of office for the day, it might appear on your calendar, but isn’t meant to keep you from meeting with others. Free/busy views also involve lower privilege. In a work scenario, as a manager, you might want to see your subordinates’ calendars but you might want them to only see your available times.
What we missed
Our original implementation of event capacity was too tricky for its own good. We soft deleted records using the
paranoia gem to track canceled interviews. We had a
belongs_to relationship with optimized database queries that leveraging
counter_cache to track the number of related records.
That’s straightforward enough, but we needed to soft delete both times interviewers are available and reservations against that time. We would then use paranoia’s
with_deleted scope to retrieve these records and surface cancellation history.
Turns out this is a trap. Remember the
counter_cache? The scope of
with_deleted is considered when updating the
counter_cache, causing canceled interviews to fail to decrement on soft delete!
Improving the data model
We leverage ActiveRecord’s
store_accessor as a way to extend models. In our
store_accessor we saved a JSON object representing a connection to a recruiter’s calendar. When this attribute was written to the database, ActiveRecord would serialize this object, and when it was time to use it in our application, we used
 accessors to retrieve data. Turns out, having a JSON object serialized to a string in the database makes it hard to query! We couldn’t simply look up a recruiter based on the information returned from Cronofy Availability API without loading all the records to memory and filtering within the app code.
While having flexibility is nice,
store_accessor can create challenges when trying to locate records. Creating a related object allows the side effect of marking a recruiter unavailable for interviewing when a calendar is unlinked—an important step in ensuring proper availability requests. Accessing JSON information with
 made it harder to change out the caller with an ActiveRecord object.
LaunchDarkly feature flags allow us to merge to master and push to production code that isn’t ready for prime time, avoiding the problem of long-running feature branches and enabling incremental development. All teams are free to work on different parts of the product unified under a single feature flag.
As for positive-failure reinforcement, the ease of recovering from exceptions during the initial rollout showed how proper monitoring, easy rollbacks, and automated release processes made fixes a breeze. Within minutes of release, we had indication that applicants may be at another stage in the job intake flow by the time our event fires due to heavy use of automation. We rolled back to minimize the impact, wrote a test case to prove the problem and verify its fix. A few minutes later we were re-launched and back to work rather than fighting fires.
Making a better experience for people who use our app starts small. Through leveraging interviews, feedback, and research, we learn the right thing to build. With small iterations and daily demos, we stay aligned and move quickly. By dropping feature parity as a requirement, we innovated on solutions that solve real problems. Finally, with strong QA influence we discovered ways our product has been falling short and were able to solve for it.
Calendar no longer means our calendar—it means your calendar. We took a look deeper into the core functionality and made it better. We optimized for how those who hire get their work done, so now anybody can hire at scale!