Software has a lot of interesting and complex problems to be solved. The difficulty of every problem is always contextual. A software problem assigned to a junior could be easier for a more senior engineer. Junior engineers struggle a lot at the beginning of their careers with software problems as there is a considerable gap between universities and the industry. Either way, some problems are complicated by nature or at least could be considered complicated by most people. In this blog, I will discuss techniques and practices to help you encounter challenging software problems, so let's get started.
So you get a task in work, or you are maybe working on your project, and you face a difficult problem. What do you do? What is the first thing you do when you have to solve such a problem? Well, it should be pretty self-evident by the title that the first thing you should do is to understand the problem. Now I know this sounds obvious, but you have to focus and give some time to understand it. Making any wrong assumptions in the beginning and you might end up working on completely unnecessary stuff, wasting time and other resources. You need to identify what is required to be implemented or fixed, the acceptance criteria to determine this task is done, and its value. It's also essential to look for any history related to this problem as it might add more context and help you move faster.
Understanding the problem is 50% of the solution
Since you understand the problem reasonably, you must then create a plan. You need to break the problem down into smaller chunks of work, ideally small (baby) steps. It's critical to give a bit of time to this phase because that will change the whole experience of solving the problem. You might also need to brainstorm the solution if you don't have any in mind. It would be best if you had a plan with the next steps forward listed one after the other. Having a good plan will help in many ways. It will ease communication, boost your mental health and motivation, and better visualize your progress.
❌ Bad Plan
- Solve the problem
- Present the solution
✅ Good Plan
- Understand what needs to be done
- Collect all information and requirements
- Reproduce X when Y happens
- Identify where the changes are needed
- Do Z change
- Brainstorm on the solution
- Google / Ask colleagues
- Make it better based on quality standards
- Make it more performant (or more secure)
- Verify it works
- Present the solution
The value of reproducing is enormous as it will verify your understanding of the problem. It will also show you the value from the user's perspective, giving you more motivation for the problem when you visualize it. Sometimes it might be possible to reproduce the problem. However, there might be problems that might not be worth reproducing because of cost, or it's impossible to do so. That's not a big issue, though, but most of the problems can be reproduced or represented in some way, so have that in mind for the next time.
Identify the area of work 🔎
The difficulty of the problem could come from various sources. It could be the system's complexity, the nature of the problem, the unknowns of the system, or even the lack of experience in software. After you understand the problem and you have a plan, you have to get your hands dirty. You need to identify the area of the problem and where you need to make all the necessary changes. Systems nowadays are massive, and I don't mean only in terms of length but in terms of complexity. Small components talk to each other with different interfaces, many dependencies, libraries, communication protocols, etc. Identifying the area of work in can be challenging.
The main idea here is to
assume -> try -> verify. Start by making an assumption based on your experience or knowledge, poke the system in any way and verify that your assumption is correct.
1. Debugger 🐞
You can assume the area of work, place a breakpoint and then trigger the system. The trigger will depend on the system's interface connecting it to the external world; it could be clicking a page, sending an HTTP request, or uploading a file. If the breakpoint hits, then your assumption is correct. If not, then you can try a couple of times more. If the debugger is not a way to verify your hypothesis, you can always move to the following technique.
2. Remove/Add code 🧹
This is a hacky solution, but it can be beneficial if the debugger is not helping you. Remove a piece of code and see if it affects the system as you expected. If it does, that will verify your assumption and quickly identify the area of work.
You can always add a log to the console or something similar to see if anything is presented. Touch the system and try things as quickly as possible so you can move to the next step of solving the problem.
3. Search for terms 🕵️♀️
When you have no idea where to start and cannot make any assumptions, it's wise to search in the system terms mentioned in the problem. For example, if the problem is "aggregate X items based in Z attribute," you might want to search for
Z in the system. There is a big chance that you will find files named after these terms, functions, or even words. That could give you a starting point to make assumptions and iterate in the loop of
assume -> try -> verify.
Time for the solution - Focus 🎯
Assuming you know where to work, you now have to solve the problem. Where do you start? Do you have any solution in mind? The first thing you should do when solving a tricky problem is to solve it directly. You must drop your standards and forget about performance, maintenance, and quality. This will help you focus more on the problem rather than non-functional requirements. Can you make the simplest dirtiest solution to tackle the problem? Then that's good, do it!
If it works, you obviously come back and clean up yourself, but if you are still stuck, you might want to do a google search.
Suppose you lack some knowledge that you know it's available on the internet. In that case, you should google and educate yourself first. If you cannot come up with a dirty solution, you might need to ask for some direction. It's essential to timebox this step since it's easy to end up in a rabbit hole searching on the second page of google (please don't do that). You should ask your colleagues if you haven't found anything there.
Ask your colleagues if you haven't found something on google. There is no point in searching on the internet if you are sure you can get the information from a colleague. You have to keep in mind that you should spend your time efficiently. Always try to balance the value with cost, you must act as a professional.
You must try to attack the problem in different dimensions. You might want to timebox every step when trying to solve a complex problem. It's easy to get absorbed and not manage your time effectively. Your brain consumes a lot of energy when something is challenging because emotions take over minute by minute. Take a step back, get away from your screen, and have a break. You won't believe how valuable this is, as your brain refreshes and sees things from a different perspective.
Taking a break can lead to breakthroughs.
You need to have a mentality of control. It's easy to get overwhelmed emotionally. Frustration and anger can rapidly increase when trying to solve a difficult problem if you are not in control. To keep control, you need a good plan. You must stick to your plan, update it if necessary and regularly check it. This helps you visualize your progress and reward your brain with serotonin as you accomplish small things step by step. Regular breaks will recharge you and help your brain see the problem from different perspectives.
There is no magic trick to solve challenging problems. It all comes down to planning, self-managing, time-efficiency, collaboration, and an attitude of professionalism. Next time you face such a problem, try to identify your way of tackling it. Do you timebox specific actions? Do you control the way you are moving forward? Do you give your brain enough breaks? If not, maybe you should reconsider your methodology and try the abovementioned techniques.