Knowing When Not to Write Code
Knowing When Not to Write Code
A software engineer’s perspective on choosing what is worth building
I write code every day. I work with requirements, build features, investigate bugs and turn ideas into software that people can actually use.
There is something satisfying about that work. A problem starts as a conversation, takes shape in an editor and eventually becomes a working feature. You can click it. Test it. Show someone what changed.
That makes writing code an easy place to look for progress.
The harder part is deciding whether the feature should exist in the first place.
Before creating a new branch, there is a question worth sitting with: what actually needs to change for the person using this?
The answer might lead to a new application. It might lead to a setting in software the business already pays for. It could also lead to removing a step that has been frustrating everyone for months.
As an engineer, I want to make that decision deliberately. Development time is valuable, and the business has to live with what we build long after the excitement of launch.
Knowing when to pause, investigate and leave the keyboard alone for a moment is part of doing the job well.
Start with the Problem Behind the Request
Imagine an operations team asking for a new dashboard because requests keep getting stuck in the approval process.
It is easy to picture the implementation: a page showing pending requests, a filter for overdue items and notifications for the people who need to act. Soon we are discussing database fields and where the new menu item should go.
Before that conversation gets too far, I would want to sit with someone who handles those requests and follow one from beginning to end.
Perhaps requests arrive through several channels. Perhaps nobody consistently assigns an owner. Perhaps “pending” means waiting for a manager in one department and waiting for missing information in another.
In that situation, a dashboard could make the confusion more visible while leaving its cause untouched. A reminder cannot reliably reach the right person if the business has never agreed who that person is.
The useful starting point would be to define ownership, agree on statuses and make the next action clear. We should also agree what improvement looks like: fewer overdue requests or less time chasing updates. Then we can assess whether the current system already provides the visibility the team needs.
A custom dashboard may still be worthwhile. We have simply given it a clearer job.
This is why I like asking people to show me the work, including the spreadsheets and messages around the official process. The request tells us what someone thinks might help. Watching the work helps us understand why they need help.
A good requirement describes the outcome clearly enough that we can consider more than one way to achieve it.
“Help the team identify and resolve overdue approvals” leaves room for investigation. “Build a dashboard with six charts” commits to a particular answer much earlier.
Simplify the Work Before You Automate It
Once we understand a process, the next temptation is to automate every step.
Suppose that approval workflow includes a spreadsheet which someone updates manually, even though the same information already exists in the main system. Another person then checks the spreadsheet and sends a reminder.
We could write a job to copy the information, another to check it and a third to send the message. We could also ask why the spreadsheet exists, who relies on it and whether that extra copy is still necessary.
Removing an unnecessary handoff may solve more of the problem than making the handoff faster. Equally, there may be a valid reason for the separate record. The point is to investigate before preserving the whole process in software.
Google’s Site Reliability Workbook describes several ways to reduce repetitive operational work, including partial automation and changes to business processes. It also recommends measuring the work and considering the cost of reducing it.
For a business team, that might mean clarifying an approval rule, reducing duplicate data entry or using one shared source of information. For an engineer, it might mean fixing the underlying cause of a recurring support task.
There is still real technical thinking involved. We have to understand dependencies, exceptions and what people need to keep doing. Sometimes the most useful result of that investigation is a smaller development scope.
Remember That Every Feature Has a Life After Launch
A feature can look small when we describe only its most successful path.
Take an export button. Someone clicks it and downloads a file. That sounds straightforward until we work through the behaviour around it.
Who is allowed to export which records? What happens when the dataset becomes too large for a normal web request? If the export runs in the background, how does the user know it has finished? Where is the file stored, and when should it be removed?
Then the data model changes. A column is renamed. A permission rule becomes more restrictive. Someone reports that the dates look wrong in their spreadsheet.
Those details belong to the feature too.
Useful software deserves this care. The reason to think about it early is to make an informed commitment. An estimate that ends at “the button works” leaves out a significant part of the decision.
The team needs to consider testing, deployment, support and the knowledge another developer will need to change it safely. If a feature depends on a particular person remembering how it works, that deserves attention as well.
The business also has an opportunity cost to consider. Time spent building a second reporting tool is time unavailable for another valuable improvement. Both ideas might be good; choosing between them still matters.
When someone asks how much a feature costs, the conversation should include what it takes to own it. That makes it easier to recognise when a focused custom build is justified and when an existing capability is already sufficient.
Look Closely at the Tools You Already Have
As someone who works with Django, I have a practical reason to check the framework before inventing another implementation.
Django provides an admin interface designed for trusted users managing application content internally. Its own documentation also makes the boundary clear: a specialised workflow may call for custom views.
Suppose a small internal team needs to find records and make straightforward updates. I would evaluate that existing interface before proposing a separate administrative application. If the team needs a guided process with carefully controlled actions, I would assess a purpose-built interface instead.
That is a decision about fit.
The same thinking applies to authentication. Django already provides foundations for accounts, groups, permissions and sessions. Using those foundations still requires decisions about access, configuration and the additional controls the application needs. Reimplementing the foundations should have a clear reason.
Business software deserves the same inspection. Before building a reminder service, check whether the existing platform can send the required reminder. Before designing a reporting portal, check its saved views and exports.
Sometimes the answer is hidden in a setting. Sometimes a feature exists but is unsuitable, too limited or unavailable under the organisation’s current plan. Either finding is useful.
The effort of checking is part of engineering. Reading documentation, testing a configuration and talking to users can move a project forward without producing a large pull request.
Put Custom Code Where It Earns Its Place
“Use an existing tool” and “build custom software” are often treated as opposite ends of a decision. A useful solution can contain both.
Consider a business that wants customers to book a specialised service. An existing product might handle the basic calendar. The business may need custom logic to decide which staff member, equipment and location can fulfil a particular booking.
That logic may be central to how the business operates. It could deserve careful custom development, while the surrounding capabilities come from established tools.
Another example is a marketing team that repeatedly needs developers to change product descriptions on pricing cards. A reasonable design might let the existing content management system handle the display copy, with the application or billing system remaining responsible for the actual subscription rules and charges.
There may be development work to establish that separation. Once it is in place, an ordinary wording change can follow an appropriate content approval process instead of requiring a software release.
The aim is to decide which changes should require an engineer and which should be manageable through a well-designed interface or configuration.
This is where understanding the business becomes especially valuable. Two organisations can request something called a “booking system” and need very different solutions. One needs familiar scheduling features. The other has operating rules that existing products cannot adequately support.
Custom code earns its place when we can explain the important requirement it meets and why the available alternatives fall short.
An Integration Still Needs an Engineer
Using an existing service can reduce what we need to build. It also introduces a relationship with another system that we have to understand.
For any proposed integration, establish how it behaves when the other service is unavailable. Can we retry an operation? Could a retry create duplicate work? How will someone recognise and resolve a failed request?
Suppose an external platform sends our application a notification that an order has changed. In designing that integration, we should establish whether notifications can be repeated or arrive out of sequence. Our response needs to fit the provider’s documented behaviour and the business consequences.
The code that connects two products may be small. The responsibility can still be substantial.
There are commercial questions too. Consider the cost at the expected usage level, access to the data, important limits and the work involved in leaving the service later. A convenient starting point can become restrictive if those assumptions are overlooked.
Cloud services require the same judgment. AWS recommends evaluating managed services as a way to reduce parts of the security management burden, while considering total ownership cost and understanding the responsibilities that remain.
Make that ownership boundary explicit. The team should know what the provider handles, what the application team handles and how changes will be managed.
The same principle applies to visual automation tools. Someone still needs to own the workflow, its permissions and its failures. Choosing a tool changes the work; it does not make ownership disappear.
Give Future Features a Reason to Exist Today
There is a phrase that deserves a little attention in development discussions: “We might need it later.”
That can be a valid concern. Some decisions are expensive to reverse. Known requirements for data separation, access control or recovery should influence the design from the start.
Other times, “later” is doing a lot of work. We begin designing a configurable rules engine for a process with one agreed rule, or support for several integrations when only one is planned.
YAGNI, short for “You Aren’t Gonna Need It,” is a useful engineering principle here. Martin Fowler explains it as a reason to defer capabilities built for presumed future needs. He also emphasises that keeping code maintainable, refactoring it and testing it remain necessary.
My practical question would be: what evidence makes this capability necessary now?
A signed requirement, a known operational constraint or an expensive future migration deserves a different discussion from a possibility nobody has validated.
We can also leave sensible room for change without implementing the whole imagined future. Clear responsibilities in the code and a straightforward data model may be enough for the current scope.
Deferring a feature should come with a record of why we deferred it and what would change the decision. That gives the team a useful answer when the subject returns.
“We will revisit this when a second integration is approved” is more actionable than an idea sitting indefinitely in the backlog.
Test an Uncertain Idea Before Committing to a System
Some requests are uncertain because the team does not yet know how people will use the proposed feature.
Imagine a business considering a portal where customers can generate detailed monthly reports. Before building account screens, report scheduling and download histories, it could speak to prospective users and test sample reports with them.
For a limited, appropriate pilot, someone might prepare the reports using existing approved tools. The team can then learn which information people need, how often they need it and what decisions it helps them make.
Perhaps customers mainly want a short exception summary. Perhaps they need a different breakdown from the one proposed. Those findings can materially change what is worth building.
A manual pilot needs boundaries. Give it an owner and a review date, protect the information involved and track the effort it consumes. It should produce learning without quietly becoming an indefinite operational burden.
Automating becomes easier to justify when the workflow is understood, demand is demonstrated and the cost or risk of continuing manually is clear.
This also creates a healthier conversation with stakeholders. We can explain what we are learning now and which decision that evidence will support. A smaller first step can give the eventual system a much stronger foundation.
Measure the Problem Before Changing the Architecture
Technical requests need the same scrutiny as product requests.
“The application is slow” can quickly turn into a conversation about a new framework, another service or a complete rewrite. Before choosing any of those, I would want measurements showing where the time goes.
For example, a page might be waiting on one database query or repeatedly calling another service. If investigation identifies a specific bottleneck, the next step is to evaluate a targeted fix and measure the result.
A larger architectural change may be justified by several requirements, such as independent deployment or a boundary the current design cannot support well. Those reasons should be visible in the decision.
The estimate should also account for the operational work a new service introduces: another deployment, a network boundary and failures that have to be understood across both sides. That work may be worthwhile. It belongs in the decision.
There is a similar discipline around old code. Before removing a feature, find out who uses it and what depends on it. Before preserving it forever, check whether it still serves a purpose.
Simplification can mean a small code change, a carefully planned removal or a larger redesign. Evidence helps us choose the right scale of intervention.
AI Makes the Decision Before Coding Even More Valuable
Even if an AI assistant produces an implementation quickly, we still have to decide whether the implementation belongs in the product.
A generated dashboard cannot establish that a dashboard is what users need. A generated integration cannot decide which system should own a business record. Those questions require understanding the context around the code.
The same review questions apply regardless of how the code was produced. Does the behaviour match the requirement? Are permissions appropriate? What happens on failure? Can another developer understand and maintain it?
For me, the opportunity is to use assistance to explore and deliver a well-chosen solution, while giving proper attention to the decisions that shape it.
If producing code becomes easier, choosing what to keep becomes an even more deliberate part of the work. We should still be able to explain why a feature deserves its place.
A Practical Pause Before the Next Build
Before committing to development, I would work through these questions with the people who will use and own the solution:
- What outcome should change, and for whom? Describe the task or frustration in terms the user recognises. Agree on how to tell whether the change helped.
- What is causing the problem today? Follow the actual work. Include missing information, unclear ownership and the workarounds outside the main system.
- What can the existing tools already do? Check the relevant capabilities and test their fit. Record the gaps that matter.
- What evidence supports building now? Look for demonstrated demand, recurring effort, important errors or a requirement with a real deadline.
- What must the solution protect and support? Establish the necessary permissions, data handling, reliability and accessibility before choosing an approach.
- Who will own it after launch? Include support, costs and future changes, whether the solution is custom code, a subscription or a visual workflow.
- What is the smallest responsible next step? It could be configuration, a pilot, an integration or custom development. Give a deferred decision a clear review point.
This should be a focused conversation proportionate to the decision. A small internal improvement should not need weeks of analysis. A business-critical system deserves a deeper assessment.
The purpose is to arrive at development with a clearer problem, a justified scope and fewer assumptions hidden inside the solution.
Build the Part That Matters
Return to the team with overdue approvals.
After investigating, we might discover that clearer ownership and an existing report are enough. We might need a small integration to bring information together. Or we might find a strong case for the custom dashboard originally requested.
Each can be a good result if it helps the team do the work and fits what the business can support.
At Tati Software, we help businesses decide whether to adopt existing software, configure or extend it, or build a custom solution. We start by understanding the need and assessing the options.
That is the kind of conversation I want to bring to software development. I want to understand what is worth solving, make the tradeoffs clear and put engineering effort where it will be useful.
There is plenty of meaningful code to write once we have done that work. The clearer the purpose, the easier it becomes to explain what we are building and why.
Planning a new feature, an integration or a business system? Talk to Tati Software about the problem you want to solve. We can help you assess the options and choose a practical route forward.