How Imprint Conducts Tech Interviews
Are you tired of software engineering interview questions that ask you to apply never-used data structures and algorithms? Are you bored of practicing leetcode questions over and over? Although the tech industry has evolved rapidly during the past few decades, the format of software engineer interviews has barely changed. We’ve all been through this frustration, and it confuses every one of us why engineers should have to answer questions that are unrelated to their daily work.
That’s why at Imprint, we’re trying things differently, to fairly evaluate our candidates and provide you with a better interviewing experience. In this blog post, we discuss some of the areas we focus on in our technical interviews.
Let’s take a look at a sample question that we ask:
Given a user profile, implement a decision engine component to determine whether this customer can be approved for a card application. After the decision is made, save the decision into a database.
The user profile has the following attributes: <span class="code-snippet">UserID</span>, <span class="code-snippet">Name</span>, <span class="code-snippet">DOB</span> (MM/DD/YYYY), <span class="code-snippet">Address</span>, <span class="code-snippet">City</span>, <span class="code-snippet">State</span>, <span class="code-snippet">ZipCode</span>, <span class="code-snippet">Phone</span>, <span class="code-snippet">SSN</span>, <span class="code-snippet">CreditScore</span>, <span class="code-snippet">MonthlyIncome</span>, <span class="code-snippet">MonthlyRentalCost</span>
The decision engine needs to implement the following rules:
- <span class="code-snippet">CreditScore > 700</span>
- <span class="code-snippet">MonthlyIncome > 5000</span>
- <span class="code-snippet">MonthlyRentalCost < Monthly Income * 0.4</span>
- <span class="code-snippet">Age > 18</span>
The database interface is as follows:
Most engineers can easily provide a quick solution, using Go for example:
Although the above solution seems to work, it’s not ideal. In the next section, we’ll share our perspective on what differentiates good, great, and superb engineers.
Good engineers have a sense of code structure and readability. The first code solution above is cluttered–it doesn’t differentiate between business logic and implementation details. When we visit a main <span class="code-snippet">func</span> like <span class="code-snippet">EvaluateUser</span>, we often ask ourselves:
- What is this function doing? (business logic)
- How is this function implemented? (implementation details)
Separating what from how greatly improves readability.
We could leverage helper functions to describe the main business logic in 3 steps. This method also makes unit testing easier because each helper function has a smaller scope:
In addition to code structure and readability, great engineers build good abstractions because they always think ahead of the current requirements. Good abstractions make systems easy to change and maintain.
Although this first implementation started with 4 rules, we could easily envision a world requiring 100+ rules. If we used the Good Engineer’s code solution, we would have to put all 100+ rules into a single function <span class="code-snippet">getUserApprovalDecision</span>, something like this:
It would be a nightmare to maintain such a huge function. If one engineer is updating a threshold while another is adding a new rule, there could easily be merge conflicts. Or, if you’re trying to find a rule that checks the user’s address, you might need to read the entire function.
With some simple abstraction, adding a rule could just be adding a new struct, without modifying the main rule evaluation function <span class="code-snippet">getUserApprovalDecision</span>:
Superb engineers write code with observability and resilience in mind because in the real world, maintaining our systems is equally important as creating them.
As a follow up interview question, we often ask, “how would you monitor the code?” Ideally, the code itself can tell us how it is performing, i.e. using logs and metrics. In our example, we could add logs and metrics around:
- Errors thrown by functions and dependencies
- Information that is good for auditing
- Function latency
- Business metrics on approval / rejection rates
To get started, we define a logger and stats interface:
Then, we apply it into our code:
When we design systems, we also set SLA goals such as 99.9% system availability (equating to only 43 minutes and 49 seconds of downtime allowed each month). We often ask candidates how they would improve code resilience.
Resilient code is integral to achieving our availability goal. In general, resilient code means the application can recover from:
- Unexpected states, often a panic or exception
- Dependency failures
In our example, we should check whether the input (<span class="code-snippet">UserProfile</span>) is <span class="code-snippet">nil</span>. If we don’t check, we could easily get nil pointer panics, which would cause the system to stop working. In addition, when calling the database through <span class="code-snippet">DatabaseAccessor</span>, we could apply circuit breakers in the database layer to protect the decision engine, and add retry logic to enable self recovery in intermittent failures.
Here’s an example of adding retry logic:
At Imprint, we are building a highly scaled payments system to support hyper-fast growth in our business. To achieve this, we are looking for engineers who can build simple/clean solutions with good abstractions, and who keep observability and resilience in mind. Please feel free to contact us at firstname.lastname@example.org if you are interested.
Subscribe to the Imprint Tech Blog here.