Back to Articles
February 15, 2026Product Development

Lessons From Building Software Products

After more than 16 years in software, the lesson I return to is simple: understand the problem and its constraints before committing to a solution.

After more than 16 years of building software, I have worked across e-commerce platforms, custom applications, SaaS products, and newer AI-assisted systems. The tools have changed many times. One lesson has remained useful:

The most expensive code is often the code that did not need to exist.

Code can be well tested, carefully designed, and still solve the wrong problem. This happens when a team accepts a requested feature as the problem itself. Someone asks for a dashboard, so the team builds a dashboard. Someone asks for a mobile app, so the team begins choosing a framework.

The better first question is usually: what decision or task is currently difficult?

Understand the Current Workaround

Before designing a solution, find out how people handle the problem today. Workarounds contain useful information. A spreadsheet may reveal the fields people actually care about. A repeated email may show that users need a notification, not another screen. A manual approval step may exist because the edge cases are too risky to automate.

I try to establish a few things early:

  • Who experiences the problem directly?
  • How often does it happen?
  • What does it cost in time, errors, or missed opportunities?
  • Which parts of the existing process are intentional?
  • What would a useful first improvement look like?

This does not require months of research. A handful of focused conversations and a close look at the current workflow can prevent weeks of unnecessary implementation.

Prefer the Smallest Complete Solution

“Smallest” should not mean unfinished. It means solving the core problem with the fewest moving parts that can support the expected use.

A monolith is often a sensible starting point because it keeps deployment, data, and debugging close together. That is not a rule against services or event-driven systems. Those approaches become valuable when there is a concrete reason for independent scaling, ownership, reliability, or integration.

The same principle applies at the feature level. If users need a daily summary, an email may be better than a reporting interface. If a process handles ten records a week, a clear review queue may be better than complex automation.

Architecture should respond to constraints that exist, not to a future the team has imagined in detail.

Treat Complexity as a Cost

Every dependency, service, abstraction, and customization creates work beyond its initial implementation. It must be understood, upgraded, monitored, and eventually changed or removed.

That does not make complexity a failure. Some domains are genuinely complex. The mistake is adding technical complexity without removing enough product or operational complexity to justify it.

When considering a new component, I ask what responsibility it owns and what becomes simpler because it exists. If the answer is vague, the boundary probably is too.

Refactoring matters for the same reason. It should not be a recurring campaign to make code aesthetically perfect. It should keep frequently changed parts of the system understandable and reduce the risk of the next change.

Engineers Need the “Why”

An engineer who understands the desired outcome can challenge an expensive assumption. An engineer who only sees a ticket can only implement the ticket.

Product thinking does not mean every developer becomes the product manager. It means the team shares enough context to make good technical decisions: who the user is, what success looks like, what can fail, and which constraints cannot move.

That context improves scope discussions. It also makes tradeoffs explicit. A fast temporary import tool, for example, should be built differently from a customer-facing workflow expected to run for years.

Software creates value when it makes useful work possible, safer, or less expensive. The craft is not measured by how much technology a team can introduce. It is measured by how clearly the system solves the problem and how well it can adapt when the team learns something new.