How to Hire Great Devs: Interview Pairing with OO design and TDD
I love interview pairing.
In 10+ years of interviewing developer candidates, it’s the most reliable technique I’ve found for hiring the good ones. Without it, you’re just guessing. And if you’ve ever hired a bad dev, you know that guessing is incredibly expensive, both financially and in team morale.
Here, I’ve enumerated The Principles, The Pattern, and The Process behind a simple guarantee: you absolutely can hire good devs every single time.
The Principles
So what makes a good developer? This part is debatable, but here are principles that I require of any developer I sign off on:
- Good developers write responsible code that doesn’t do more or less than is necessary. It’s simple and self-documenting where possible.
- Good developers speak the language of software development. They can speak intelligently on encapsulation and coupling and scope and so on.
- Good developers understand abstraction and object design. Not every system design requires object-orientation but the majority will benefit from it.
- Good developers can work with others. They are opinionated (as any good engineer should be) but they are open to suggestions and alternative solutions.
- Good developers can describe their problem-solving process. They can tell you why we need a property and how we might make a function recursive and so on.
- Good developers can debug their code. They know how to read a stack trace and how to take small, effective steps to uncover the source of the bug.
- Good developers recognize opportunities for refactoring. They know refactoring patterns and use them appropriately.
These are non-negotiables for me and for our software team.
Interview pairing reveals principles in a candidate that are impossible to reveal in a non-programming interview.
The Pattern
For 60–90 minutes, we write code with the candidate, working on a system with increasingly challenging requirements.
Before doing any design, we review a narrative together that describes the system we’ll be building. This can be a toy problem or you can use a harness that mimics actual requirements you might have for your production domain. That’s up to you.
Before writing any code, we do up-front design together. We brainstorm objects and identify what we think their private members and public interfaces might look like. Whenever possible, we do this on a physical whiteboard and we use an established architecture diagramming technique such as hybrid UML. We talk about design trade-offs and note any concerns.
Then we write code. We ping-pong pair in one direction. This means the interviewer writes the tests (we’re TDDing remember) and the candidate passes the tests. This provides a clear delineation of responsibility and clear expectations of the candidate’s role.
We use an established unit testing framework. For C#, this is MSTest or NUnit. For JavaScript, we use Mocha. For PHP, we can use PHPUnit. And so on.
We test-drive using “red, green, refactor”. We start with a failing test. The candidate writes code to pass the test. Then we present the candidate an opportunity to refactor. Then we write our next failing test. And so on.
We write code using the tools that will be used on the job in an environment as close as possible to actual work conditions.
With that pattern in mind, let’s go step-by-step…
The Process
Here’s the basic script I use.
First, start with the introductions and ground rules.
“Hi, candidate. I’m Mark. Any experience with TDD or unit testing? How about object-oriented design? Ok, cool. Today, we’ll be building some software together. First, we’ll do some up-front design. Then, we’ll test drive our code. I’ll write the tests and you’ll pass them.”
Important points:
Next, we look at the problem we’re trying to solve.
Ideally, we want a scenario that scales from easy to difficult with ample opportunities for refactoring.
Here’s an example:
Our client, an online retailer, wants to begin selling movies.
Since they have an existing system, they want us to build a new service for handling movie pricing and totals.
Our client’s web site should be able to call our service each time a movie is added to a customer’s cart. When they’re done adding movies, they should be able to get a total from our service.
To start, they’ll only sell Star Wars movies. To encourage sales, they want to discount purchases of multiple movies in the series but only if the movies are different Star Wars episodes.
Here’s what they’ve told us about the pricing and discounts:
One copy of any of the 6 Star Wars movies costs $10.
If you buy two different Star Wars movies, you get a 5% discount on those two movies. Purchasing A New Hope and The Empire Strikes Back, for example, would cost $19.
If you buy 3 different Star Wars movies, you get a 10% discount. So buying one copy of each of the prequels, for example, would total $27.
If you buy 4 different Star Wars movies, you get a 20% discount.
If you buy all 6 Star Wars movies, you get a 40% discount.
Note that if you buy four Star Wars movies, of which 3 are different episodes, you get a 10% discount on the 3 distinct movies, but the fourth movie (the duplicate) still costs $10. See below.
Example Order
1 copy of Star Wars Episode IV: A New Hope
2 copies of Star Wars Episode V: The Empire Strikes Back
1 copy of Star Wars Episode VI: Return of the JediIn this case, A New Hope, Return of the Jedi, and one copy of The Empire Strikes Back should be discounted 10%. The second copy of The Empire Strikes Back is full price because we only want to discount purchases of different movies in the Star Wars series.
So the total for this group of movies is $27 + $10 or $37.
Bonus: We should always give the largest discount possible.
What makes this a good problem?
Notice that it starts off very simply with data structure and collection operations. But once those are solved, we get into some interesting grouping and comparison algorithms with plenty of margin for creative approaches.
This is important: starting simply lets the candidate settle into the process and allows us to get comfortable working together.
But we also want an open-ended problem that can’t easily be solved. Many toy problems are just not deep enough to expose flaws in design and technique. So, sorry all, but FizzBuzz is out.
Now we design.
Again, we use a physical whiteboard if at all possible. I like relaxed UML, so I usually do something like the following:
Notice that our Movie implements an interface called Item. This is one of many examples of a practice I’d try to coax out of a candidate. I’ll typically talk through some scenario in which we have access to our client’s existing code base and that it contains something we can either reuse or extend. It’s the candidate’s job to identify what that something is.
For this particular exercise, we’d probably have UML for handling discounts, but we might just as easily defer it until some design emerges during TDD. It depends on the candidate and doesn’t actually matter all that much.
What does matter is that they can design simple objects and understand their dependencies.
Finally, we code.
Almost… I just want to note that everything we’ve done to this point is also part of being a good developer. It’s not just writing code.
For a first test, I think I’d want to know that we have a service and that it correctly returned a total of $0 when we haven’t added any movies. Assuming we’re using C# and NUnit, I might write a first test that looks like this:
// MovieServiceTest.cs[Test]
public void CalculateTotal_GivenZeroMovies_ShouldReturn0()
{
MovieService movieService = new MovieService();
Assert.AreEqual(0m, movieService.CalculateTotal();
}
The candidate would pass this test by creating the MovieService class (if it doesn’t already exist — sometimes I give them the first class for free) and adding a CalculateTotal() method to it. Sort of like this:
// MovieService.cspublic decimal CalculateTotal()
{
return 0m;
}
Maybe our second test looks like this:
// MovieServiceTest.cs[Test]
public void CalculateTotal_GivenOneMovie_ShouldReturn8()
{
MovieService movieService = new MovieService(); movieService.Add(new Movie()); Assert.AreEqual(10m, movieService.CalculateTotal();
}
Which the candidate could pass by creating a Movie class and modifying the MovieService class like so:
// MovieService.csList<Movie> movies = new List<Movie>();
public decimal CalculateTotal()
{
return movies.Count * 8;
}public void Add(Movie movie)
{
movies.Add(movie);
}
And then we TDD the system until our time is up.
There are obviously many ways to approach these. Any developer who’s done TDD knows that every practitioner does it differently, but that’s a topic for another essay.
Where Do We Go Now?
There are many details and examples I can give, but hopefully this is enough to get you started.
There is no substitute for knowing with certainty that the developer you’re about to hire is actually worth hiring.
Good luck!