I've always found Sherlock Holmes to be an interesting character in stories. Ever since that first story I've been fascinated by the detective and can't get enough of reading his stories as told by Dr. Watson. So much so, that I've come up with a few principles to take from the great detective that I apply when it comes to software engineering.
Both Holmes and great developers solve complex problems by gathering comprehensive information, forming theories based on evidence rather than assumptions, tracing issues to their source, and collaborating to validate their reasoning.
Whether you're planning a new feature, debugging a production issue, or architecting a system, here are a few ways to apply the skills of the detective to your next goal.
Gather All the Facts First
"Before I can judge what is or is not relevant, I must know all the facts."
The first thing Holmes does when it comes to solving a case (often before he decides if he will take a case) is to ask questions. Lots of them. Questions that often would lead to the potential client asking "Why are you asking me this?". This very thorough initial inquisition would lay the groundwork for everything about the case that was to follow. In development, this means exhaustive questioning during planning, before you write any code.
I'm often cited for asking great questions both before and during a new implementation. Questions like:
About Users:
- Who exactly will use this?
- What device will this primarily be used on?
- What is the foundational problem that we want to solve?
About Constraints:
- What type of performance is expected?
- Are there any compliance requirements? (HIPAA, GDPR, SOX)
- What data do we need to implement this and who owns it?
About Our Product:
- Is this something new or something we are expanding on?
- Will we need to integrate any new technology to implement this?
- What failure mechanisms should we have in place if x assumed service stops working?
These types of questions can reveal crucial constraints and expectations. Ask as many as you can think of. Not just to figure out what you do need, but sometimes more importantly what you do not. Just as Holmes investigating a victim's family history might reveal a crucial detail that helps solve the current case, thorough questioning helps to get to the right answers that can get rid of problems before they occur during development.
Let Facts Shape Your Theory, Not the Other Way Around
"It is a capital mistake to theorize before one has data. Insensibly one begins to twist facts to suit theories, instead of theories to suit facts."
Holmes never formed theories before gathering evidence. He invariably gathered facts as a case went on and never preemptively formed a thesis beforehand. In development, this means avoiding over-engineering and abstractions before they are necessary.
Sometimes we as developers have a bad habit of over-engineering a service in the name of "scale". Let's say you have a need to implement a queue into your application. You first impulse should not be to look towards a third party library, or an entirely new service to bring into your app. For me it would be to implement your own simple queue. The answer will depend on the number of users and how large you expect this queue to get, but that should be determined in the planning phase. If a simple queue will be enough to satisfy your current use-case, start there. Iterate on your solution and let the more complex solutions only be brought in as necessary. Every new service you bring into the app is a new tool the team must learn and a new tool that you are the mercy of if they decide to update or change any of their feature-set.
I also apply this to creating abstractions. I favor localization and keep functionality close to where it's needed. If error handling logic is only used in one component, it doesn't need to be abstracted into five different files. If a utility function serves only the user profile page, keep it in that module until you need it elsewhere.
This approach prevents the classic developer trap of building elaborate solutions to problems you don't actually have. Start simple, add complexity only when the facts demand it.
Start at the Source
"When you have eliminated the impossible, whatever remains, however improbable, must be the truth."
Holmes liked to find out what the motivation for a criminal would be. Asking questions and investigating to get to the initial driving factor that would lead to whatever crime there's been. Similarly, when debugging, I like to start at the data source, not where the error appears.
Here's a real scenario: users report that their dashboard shows yesterday's sales figures instead of today's. The UI may indeed be displaying the wrong data, but debugging the UI first is like arresting the first person at a crime scene.
Instead, I like to trace starting from the source. Especially if it's a part of the application you're not as familiar with. It can look something like this:
- Database: Run the query manually. Does it return today's data?
- API: Hit the endpoint directly. Is the backend serving correct data?
- API Communication: Check dev tools network tab. Is the request being made correctly?
- Caching: Are we serving any stale data from the cache?
- State Management: Is the component receiving correct data but not rendering correctly?
In this example, you might discover that the data in the db is correct, but the api has an issue with a timezone conversion and is sending the data from the wrong date. Starting at the UI often times assumes that the other pieces of the application are completely working correctly. Unfortunately, in my experience that is not an assumption you can make when determining the root cause of an issue.
Holmes investigated a victim's entire history to understand the motivation for a crime. So should we investigate the complete data flow of our systems to understand the source of errors. Eliminate the impossible explanations systematically until you're left with the truth, however unexpected.
Have Your Watson
Holmes relied on Watson not as a sidekick, but as a crucial partner in his investigative process. Explaining his reasoning to Watson forced him to articulate his logic and could reveal flaws in his reasoning or spark new insights based on Watson's feedback.
Every developer needs a Watson when the moment calls. My rule: if I'm stuck for 30 minutes with no clear path forward, I find someone to talk through the problem.
Other times you may need a Watson:
- Pair programming: Working through the problem together in real-time to find a solution.
- Code review: Getting someone else feedback on your solution can either validate your approach or reveal a gap you need to close.
- Getting Unstuck: Explaining your train of thought once you've hit a roadblock to someone else can reveal a crucial point you've overlooked that can get you to the outcome you need.
Your Watson doesn't need to be more senior than you. Sometimes a junior developer asking "Why did you choose this approach?" makes you realize you're overcomplicating things. Sometimes a colleague from another team can spot an integration issue you missed because you were too close to the problem.
The key is being able to articulate your reasoning and being open to feedback. Do not tie yourself to any code. Be focused on the outcome and not your solution. This forces you to examine your assumptions and can give you more confidence in the solution you've chosen. Your teammates can help you the same as Watson helps Holmes see cases from new angles.
The Deductive Developer
These are four principles I like to follow when it comes to Deductive Development: gather information before coding, build only what the requirements demand, trace problems to their true source, and collaborate to validate.
The next time you face a complex development challenge, channel Holmes' methodology. Ask exhaustive questions during planning. Let the facts shape your solution rather than forcing facts to fit your preconceptions. When debugging, start at the data source and work your way up. And when you're stuck, find your Watson to help you see what you might have missed.
The best developers, like the world's greatest fictional detective, understand that methodology trumps genius. When you approach your next development challenge, remember that every feature request is a case to be solved, every bug is evidence waiting to be interpreted, and every system is a mystery with logical rules governing its behavior.
After all, there's no mystery so complex that it can't be unraveled by the right questions, the right evidence, and the right methodology.