Introduction
Junior engineers are often confused about the right decisions to make, especially with problems they’ve never encountered before. This is extremely prevalent with beginners.
The conventional misconception is that you need to learn everything in order to do your job well.
Unfortunately, this is one of the many ways to burnout and will only amplify imposter syndrome. While there are a plethora of things to learn, there is limited time. Not everything you learn will necessarily be relevant. You must be selective with your time and attention.
In this article, I’ll go through the mental framework you should use to tackle hard problems alongside a concrete example from my own experience.
Clarifying The Problem
The best way to approach problems of an ambiguous nature is to start by defining the problem and clarifying the desired outcome. Almost all problems have an end resolution that you’re moving towards. Without an end target, it’s difficult to know if you’re making progress or not.
Actionable Steps
Once you’ve identified an ideal outcome, you need to clarify the problem by breaking it down into actionable steps. This is the part where most beginners fail. If I asked you “What are your next actionable steps?”, how would you answer that question?
If you’re unable to come up with a good answer, it means you lack judgment to discern what “good” means. In this context, we want to define steps that are:
- Moving closer towards the outcome
- Tangible action(s) that are achievable
For example, at my previous workplace, I identified a bug that was causing an ingestion service to fail. I noticed there was a huge memory spike on Datadog that said the service was hanging and would freeze indefinitely. I wasn’t sure what was causing it though.
A poor next step in this case would’ve been: “I need to solve this bug by investigating further” – This is rather ambiguous and unachievable. There’s no clear definition of what this even means, which is why so many engineers waste time by meandering through code when this is all they have. How would you justify this to someone else? You need to dig deeper and clarify.
Here’s a better-defined action item: I’m going to run the debugger step by step and see where the program terminates. From there, I can isolate and narrow down the failure.
As you can see, this is a much better step. It’s actionable and gets us closer towards our end goal.
Blockers
In a perfect world, you have all the correct steps but this is rarely the case. More often than not, you’re going to run into blockers. Here, I want to highlight that problem-solving is not so much of a linear journey. You will constantly need to adapt as you’re moving along. And while you may know your end destination, it is still your job to adapt to the problems that show up.
In the example above, I was running into a blocker while debugging. It turned out executing the debug process was much harder than I had anticipated due to the lacking documentation. One option was to look up how to do it but I had concluded that it was far too expensive to figure out, especially due to the nuances of the environment.
Even though I had a potentially good plan, I was unable to act on it which threw me back to square one.
This is why you need to develop a good decision making tree for approaching problem types, i.e. collecting decision making frameworks for the problem domains you’ll face. The difference between having a solid decision-making framework versus not is the difference between knowing what to look for versus going in blind.
For that, we’ll need brainstorming as an essential process towards resolving blockers like this.
Brainstorming
In the context of brainstorming, it means laying out the relevant cards, considering the options that are available to you, and then filtering down to the next best action step. Additionally, it’s also important to consider the costs for implementing solutions as every action requires a risk assessment.
In the example above:
- I decided to deploy the program instead, and added logs around the area of concern that I thought was making the program fail.
- Since the program was ingesting large files, it made sense that the memory spikes were occurring around the area where the file was being opened.
- To verify if this was the case, I installed a library that measured the RAM usage and added logs before, during, and after the process where the file was being ingested.
The result? The logs had confirmed my suspicion.
It turns out the script was using a library that was streaming the entire file into memory and was causing this nasty memory spike. Had I stuck to my guns and tried to figure out how to use the debugger, I may have spent an additional 3 hours or so with nothing to show for my time. You must always keep the bigger picture in mind and consider the cost of your solution.
Side Note: The biggest mistake I see junior engineers make is to assume the cause of the problem without ever verifying its truth. There is a high cost for solving problems that don’t exist. Had I merely assumed that code X was the issue because it “made sense” to me without verifying the cause, I would have risked going down the wrong rabbit hole.
The Cycle Continues
As you continue to problem-solve, you’ll often find yourself repeating the above cycle — encountering blockers, brainstorming solutions, and adapting your plan as you go.
For instance, even though I was able to isolate and narrow down the cause, I still needed to implement and test the new solution. Unfortunately, the deployment process was expensive. It would take 5+ minutes, and if I made any wrong changes, the service would hang and fail and I would need to restart all over again.
Instead of doing a manual deployment, I used an adaptive approach. I took the offending code and moved it over to my local environment where I could test critical parts of the code without needing to subject myself to the harsh conditions of the prod environment.
In doing so, I was able to get a much faster response time — the difference in this testing process meant I could decrease testing from 5+ minutes to 200ms and move significantly faster.
Conclusion
For successful problem-solving, awareness of high-impact activities is crucial, as demonstrated by my own experience. Not only does this catalyze effective brainstorming – i.e. generating good solutions – but also makes navigating problems 10x easier, as it allows you to pivot and adapt more quickly. Such awareness can only be obtained through skill development, personal experience, and/or observing experts in your specific field of challenge.
At the end of the day, it’s not about knowing everything, it’s about knowing who or where to get the required resources to do your job more effectively.
If you want to go even deeper into this topic, check out these other resources around effective problem solving:
I’m Jian, and if you liked this article, I’m sure you’ll like Taro, the premier resource for growing as a software engineer. If you’re interested in Taro Premium, you can get a discount by using my special referral link here: https://www.jointaro.com/r/jiank627/