7 Hard Truths Every Developer Must Face

In my journey as a software developer, I've traversed many highs and lows, grappling with complex problems and celebrating breakthroughs. Through these varied experiences, I’ve come to accept several hard truths about our profession—insights that are seldom taught in textbooks or coding bootcamps, but are learned on the job, often the hard way. These truths span the technical, the tactical, and the personal aspects of our work.

Whether you’re just starting out or have been in the field for years, understanding these realities can provide a sense of shared experience, reduce frustration, and guide more effective approaches to our craft. Here are ten hard truths that every developer must face, distilled from lessons learned through personal triumphs and trials in the world of software development.

The Code You "Write From Scratch" Isn't as Good as You Think

Every developer loves the feeling of starting a new project. It's a blank canvas, an opportunity to craft something clean, efficient, and impactful. Yet, there's a hard truth that we all must confront: the code we write from scratch isn't as good as we think it is.

In my early days as a developer, I would often dive into projects with a sense of unbridled optimism. The initial lines of code would feel revolutionary, as if I were writing the very algorithms that could change the industry. However, as the projects progressed, the reality would set in. Bugs would emerge, and features that seemed simple in theory would prove complex in practice. The clean slate would quickly become cluttered with patches and fixes, making me realize that my initial code was not the masterpiece I had imagined.

This realization is not unique to me; it’s a universal experience among developers. When we start from scratch, our focus is often on solving the problem at hand as quickly and as innovatively as possible. This can lead to overlooking potential edge cases or failing to consider the maintainability of the code. Moreover, without the benefit of feedback and testing, it's easy to miss flaws in our logic or design.

Here are a few strategies I’ve learned to help mitigate this issue:

  1. Iterate and Refactor: Don’t expect your first draft to be perfect. Write your initial code with the knowledge that you will return to refine and improve it. This iterative process allows you to evolve your codebase responsibly.
  2. Peer Reviews: Regular code reviews are invaluable. They bring fresh eyes and new perspectives to your work, helping to spot issues that you might have missed.
  3. Automated Testing: Implementing a robust suite of automated tests can catch many problems early in the development process. These tests act as a safety net, ensuring that changes don't break existing functionality.
  4. Continuous Learning: Stay humble and keep learning. The more exposure you have to different coding techniques and architectures, the more you will understand the complexities of writing truly great code.

Accepting that your first attempt at coding a new project might not be as perfect as you think is a sign of professional maturity. It keeps you vigilant against potential pitfalls and open to continuous improvement, which ultimately leads to developing better, more reliable software.

No One Cares About Your Tech Debt—Until It’s Too Late

Tech debt is like the dark matter of software development: it's invisible but has a massive influence on everything in a project's universe. As a developer, I've learned a harsh but valuable lesson: no one really cares about your tech debt—until something breaks.

In the early stages of my career, I often felt frustrated when the tech debt I reported seemed to fall on deaf ears. Stakeholders would prioritize new features or deadlines over addressing the mounting issues hidden beneath the surface. This approach often leads to a cycle where the tech debt continues to accumulate, unnoticed and unaddressed, until it becomes a significant barrier to progress or, worse, causes a critical system failure.

The challenge lies in the fact that tech debt isn’t immediately visible to those outside the development team. To non-technical stakeholders, it might seem like an abstract problem, one that doesn’t impact the immediate functionality or appearance of a product. However, as any seasoned developer knows, unchecked tech debt can severely impact system performance, scalability, and the speed at which new features can be delivered. It can turn what should be simple updates into a complicated mess.

Here are a few strategies to make tech debt more manageable and to help others understand its importance:

  1. Quantify the Impact: Provide concrete examples of how tech debt has slowed down projects or increased costs. Use metrics and incidents as evidence to highlight the impact.
  2. Educate Your Stakeholders: Regularly communicate what tech debt is and the risks it poses. Help stakeholders understand that while tech debt is a technical issue, its repercussions are very much business-oriented.
  3. Incorporate Debt Reduction into Your Roadmap: Treat tech debt like any other feature. Dedicate regular sprints to addressing it, making it a routine part of your development cycle.
  4. Prioritize Wisely: Not all tech debts are created equal. Prioritize the repayment of debts that pose the greatest risk or are the most likely to impact productivity or system stability. Use Tools and Automation: Leverage tools that help identify and track tech debt. Automation can also prevent new debts from piling up by enforcing coding standards and running tests automatically.

Understanding and addressing tech debt is a critical part of software development that requires as much attention as any feature or bug fix. By making tech debt visible and demonstrating its impact, you can foster a culture where it’s recognized as a crucial factor in a project’s long-term success and stability.

Technical Debt is Inevitable and Necessary

In the pristine world of software development theory, technical debt is something to be avoided at all costs. However, in the practical, fast-paced environment of real-world software development, technical debt is not only inevitable but often necessary.

Technical debt refers to the extra development work that arises when code that is easy to implement in the short run is used instead of applying the best overall solution. Early in my career, I viewed technical debt as a failing—a sign that a project was not being managed correctly. Over time, however, I've come to understand that technical debt is a natural part of software development. Like financial debt, it can be used strategically to push a project forward faster than would otherwise be possible.

Why Technical Debt is Unavoidable

  • Rapid Delivery Requirements: In many cases, the need to deliver functional software quickly to meet market demands or stakeholder expectations necessitates taking on technical debt.
  • Evolving Project Requirements: As a project evolves, decisions that once made sense can become liabilities. This evolution is normal and results in layers of technical debt that reflect the project's history and changing needs.
  • Resource Constraints: Limited resources often mean that teams must choose between perfecting a current implementation and moving on to new features that provide more immediate value to users.

The Strategic Use of Technical Debt

While unchecked technical debt can lead to increased maintenance costs and reduced system performance, strategically managed debt can be an asset. Here’s how to handle technical debt effectively:

  • Acknowledge and Track: Recognize when you're incurring debt and document it. Understanding its impact allows for better management and prioritization.
  • Communicate Clearly: Ensure that all stakeholders understand the trade-offs involved with incurring technical debt and the plans for addressing it in the future.
  • Prioritize Repayment: Just like financial debt, technical debt incurs 'interest'. Prioritize the repayment of high-interest technical debt that slows development or poses risks to future project scalability and maintainability.
  • Incorporate into Roadmaps: Plan for regular periods where the focus shifts from feature development to reducing technical debt, ensuring long-term health and stability of the codebase.

By accepting technical debt as a necessary element of software development, developers and project managers can use it to their advantage, making informed decisions that balance short-term gains with long-term sustainability. Understanding that technical debt is not a mark of poor development, but a tool in the developer’s arsenal, can transform the way teams approach project planning and execution.

Rewriting From Scratch is Rarely the Answer

One of the most tempting solutions to a developer when faced with a mountain of tech debt or problematic code is to start over—wipe the slate clean and build anew. Yet, as we've touched on in the first point, starting from scratch carries its own set of challenges and risks.

Rewriting a system from scratch can be deceptively appealing. It promises a fresh start, free from the constraints and mistakes of the current implementation. However, this approach often underestimates the complexity and the nuanced understanding embedded in the existing codebase, gained through years of adjustments, fixes, and real-world testing. Starting over can discard this hard-earned knowledge, leading to a repeat of old mistakes and the introduction of new ones.

Here’s why a complete rewrite is seldom the best approach:

  1. Loss of Institutional Knowledge: Existing codebases often contain solutions to edge cases that are forgotten until they re-emerge in the new system. These are lessons that have been learned in battle, so to speak.
  2. Underestimation of Effort: New projects are exciting, and it's easy to underestimate the time and effort required to reach feature parity with the old system. This can lead to longer development times and delayed releases.
  3. Potential for New Bugs: New code means new bugs. While fixing issues in the old system, you’re aware of most existing bugs, but with new code, you’ll have to rediscover and fix new ones, which can be more damaging than the original issues.
  4. Diverted Resources: During a rewrite, fewer resources are available for ongoing maintenance of the current system, which can lead to degraded service and unhappy users or customers.

Instead of a full rewrite, consider these alternatives:

  1. Refactor in Increments: Tackle the rewrite incrementally. Isolate parts of the system that most need improvement and refactor them one at a time. This approach mitigates risk and allows for gradual improvement without overwhelming the team.

  2. Improve the Existing Codebase: As discussed under the first hard truth, improving and cleaning up existing code is often more effective than starting anew. Focus on gradually reducing the tech debt and improving system architecture with each iteration.

  3. Document and Analyze: Enhance documentation to capture both the existing functionality and the rationale behind current implementations. Use this analysis to make informed decisions on what to refactor and what might actually need rewriting.

By advocating for incremental and thoughtful refactoring, we preserve valuable insights and minimize the disruption typically associated with starting from scratch. This approach not only maintains the integrity of the application but also aligns better with practical business needs and developer capacities.

You Need to Learn to Work With Bad Code

As developers, we often dream of working with perfectly crafted codebases where every line is a testament to best practices and software craftsmanship. However, the reality is far different. Most codebases you will encounter have their share of issues—after all, all the best codebases started out bad. It's the effort and skill of great developers that turn these rough drafts into well-oiled machines.

In the early stages of most projects, the code is often less than ideal. Deadlines, changing requirements, and multiple hands contributing code can all lead to a codebase that looks more like a patchwork quilt than a seamless tapestry. This is normal, and it's an opportunity rather than a setback.

Here's why embracing and improving bad code is crucial:

  1. Growth Opportunity: Working with challenging code is a powerful way to sharpen your problem-solving skills. Each bug fixed and each inefficient process refactored is a lesson learned.
  2. Value Addition: By improving a codebase, you're directly increasing the value of the project. Better code leads to fewer bugs, easier maintenance, and more scalability.
  3. Real-World Skill: The ability to refactor and optimize existing code is a highly valuable skill in the real world. Most development work isn't creating new projects from scratch but evolving existing ones.
  4. Team Contribution: Improving a bad codebase is a team effort and enhances your role within the team. It shows leadership and initiative, qualities that are indispensable in a professional setting.

To effectively work with and improve bad code, consider these strategies:

  1. Incremental Refinement: Tackle the codebase piece by piece. Choose one aspect to improve, such as increasing function modularity or enhancing performance in critical sections, and focus on that before moving to another.
  2. Document Your Changes: As you make improvements, keep detailed documentation. This not only helps your future self and team members but also institutionalizes the knowledge gained from the improvements.
  3. Learn From Mistakes: Analyze the existing flaws in the code to understand how they came about. This insight can help prevent similar mistakes in future projects. Seek Feedback: Regularly review your refactoring efforts with peers. Fresh perspectives can help catch issues you might have missed and validate the improvements.

Working with bad code isn't just a necessity—it's a chance to make a significant impact. By transforming a struggling codebase into a robust and efficient one, you demonstrate not just technical skills but a commitment to quality and continuous improvement.

Performance Isn't Everything—Especially Outside FAANG

In the realms of Facebook, Amazon, Apple, Netflix, and Google (FAANG), where high stakes meet large-scale user bases, performance optimizations can have monumental impacts. These environments, where even milliseconds can affect user experience and revenues, often set a precedent that many in the tech industry strive to emulate. However, for the vast majority of developers not working within these tech giants, the intense focus on high-level performance is more an exception than a norm.

Early in my career, I was captivated by tales from Google, where performance optimization seemed like the holy grail of development. It appeared that every aspect of computing required meticulous tuning and endless refinement. This perspective, however, is heavily influenced by the visibility and success of these companies. As I ventured into different environments—small startups, public sector projects, and mid-sized businesses—I realized that this narrative doesn't fit the broader tech landscape.

Here’s why performance isn’t the central focus in most development jobs:

  1. Business Needs Prevail: Many companies prioritize launching functional products quickly or adding features that directly enhance customer satisfaction and drive revenue over perfecting performance.
  2. Cost vs. Benefit: The resource-intensive nature of extreme performance optimization often does not align with the budget or practical constraints of smaller companies. The marginal gains from such optimization seldom justify the hefty investments.
  3. Scalability Isn't Universal: Not all applications need to support millions of users. Over-optimizing for hypothetical scalability can lead to unnecessary complexity and wasted resources.
  4. User Perception: In many cases, users may not notice the incremental improvements in performance. They are more likely to value enhancements in usability, aesthetics, or functionality, which more directly affect their interaction with the software.

Despite these realities, performance still matters, but it requires a balanced approach:

  • Focus on Meaningful Performance: Concentrate on optimizing elements that users directly interact with and which can genuinely enhance the user experience, like faster page loads or more responsive interfaces.
  • Monitor and Adapt: Utilize performance monitoring tools to understand real-world usage and respond to actual bottlenecks rather than hypothetical ones.
  • Educate Stakeholders: Clarify for stakeholders the practical impacts of performance improvements and align optimization efforts with tangible business goals.

The tech industry's admiration for FAANG-like optimization often overshadows the actual needs of most projects. Recognizing this can free developers to allocate their efforts more effectively, focusing on what truly matters for their specific context and challenges.

You Know Less Than You Think and Can't Know Everything

One of the most humbling experiences as a developer is the realization that you know less than you think. In the vast and ever-expanding field of technology, it's impossible to grasp every concept or master every tool. This isn't a reflection of inadequacy but a reality of our profession's complexity.

Early in my development career, I felt confident in my skills and knowledge. However, as I progressed, encountered new challenges, and interacted with peers from diverse backgrounds, I quickly understood that my expertise was just a drop in the technological ocean. This realization isn't demoralizing; rather, it's liberating. Accepting that you can't know everything encourages a mindset of continuous learning and openness to new ideas.

Why Accepting This Truth Matters

  1. Reduces Overconfidence: Knowing that you don't have all the answers helps prevent overconfidence, which can lead to mistakes and oversight in your work.
  2. Encourages Collaboration: Recognizing your limitations makes you more likely to seek out and value the expertise of others, fostering a more collaborative and innovative work environment.
  3. Promotes Lifelong Learning: The tech industry's rapid evolution makes lifelong learning a necessity. Embracing the fact that you can't know everything keeps you curious and engaged, always ready to adapt and grow.

How to Manage Not Knowing Everything

  1. Prioritize Learning: Focus on deepening your knowledge where it counts most. Prioritize learning based on your career goals and the needs of your projects.
  2. Leverage Community Knowledge: No one knows everything, but collectively, much can be known. Engage with communities, forums, and colleagues to fill gaps in your understanding and contribute your knowledge.
  3. Stay Curious: Maintain an attitude of curiosity. Approach new projects and technologies with an open mind, and don't be afraid to dive into unfamiliar territories.
  4. Use Tools and Resources: Make use of tools, documentation, and online resources to bolster your knowledge and compensate for areas where you may not be as strong.

Understanding that you know less than you think, and that you can't know everything, is not just a hard truth but a cornerstone of growth and humility in the life of a developer. It keeps you grounded, constantly learning, and respectful of others' expertise.

Conclusion

Embracing these hard truths about software development can be challenging, but it’s also a transformative part of our growth as professionals. Each truth, from the imperfect nature of our initial code to the reality of working within less-than-ideal codebases, serves as a reminder of the complexities and the demands of our field. These insights encourage us not only to strive for technical excellence but also to cultivate resilience, adaptability, and a commitment to continuous improvement.

In recognizing that rewriting from scratch isn't always the solution and that dealing with tech debt and imperfect code is part of our everyday reality, we become better equipped to handle the pressures and challenges of our profession. Moreover, by understanding that our technical skills are just part of the equation and that our non-technical skills are equally vital, we prepare ourselves to contribute more effectively to our teams and projects.

Ultimately, the journey of a developer is one of perpetual learning and adaptation. As we navigate through these truths, we build not only better software but also stronger, more adaptable characters. Let us take these lessons to heart, apply them in our daily work, and continue to push the boundaries of what we can achieve as developers.

Wei-Ming Thor

I create practical guides on Software Engineering, Data Science, and Machine Learning.

Background

Full-stack engineer who builds web and mobile apps. Now, exploring Machine Learning and Data Engineering. Read more

Writing unmaintainable code since 2010.

Skill/languages

Best: JavaScript, Python
Others: Android, iOS, C, React Native, Ruby, PHP

Work

Engineering Manager

Location

Kuala Lumpur, Malaysia

Open Source
Support

Turn coffee into coding guides. Buy me coffee