15 Rookie Mistakes Even Experienced Developers Still Make

IT TrainingIT trends

YOUR COSMETIC CARE STARTS HERE

Find the Best Cosmetic Hospitals

Trusted • Curated • Easy

Looking for the right place for a cosmetic procedure? Explore top cosmetic hospitals in one place and choose with confidence.

“Small steps lead to big changes — today is a perfect day to begin.”

Explore Cosmetic Hospitals Compare hospitals, services & options quickly.

✓ Shortlist providers • ✓ Review options • ✓ Take the next step with confidence

Originally sourced from InfoWorld. Rewritten and expanded for a DevOps engineering audience.

Experienced developers are not immune to rookie mistakes. After two-plus decades in the industry, the same tactical errors appear on project after project — sometimes made by junior engineers, sometimes by veterans who should know better. These are not hygiene issues like skipping version control. These are subtle, high-impact mistakes baked into the code itself. Here are 15 of them, why they hurt, and what to do instead.

1. Using Make or Shell Scripts as a Build Tool

If you are not writing C or C++, Make is probably working against you. Make spawns a separate compiler process for each file, which is not how most modern languages are designed to operate. Resolving transitive dependencies in a language like Java using Make is painful at best and brittle at worst.

Shell scripts suffer a related problem: they are fragile across environments and break silently when upstream dependencies change versions. What feels like a quick win — skipping a full build toolchain — almost always becomes a maintenance liability. Use a purpose-built build tool for your language ecosystem: Gradle or Maven for Java/Kotlin, npm scripts or Turborepo for JavaScript, Cargo for Rust, and so on. These tools understand dependency graphs, support incremental builds, and integrate with CI pipelines.

2. Using the IDE as a Build Tool

Every major IDE — IntelliJ, Eclipse, Visual Studio — has a one-click build-and-run feature. It is fine for rapid local prototyping. It is a problem the moment a second developer joins the project or you need to run the build in a CI/CD pipeline.

IDE builds hide configuration in project files that are often gitignored, editor-version-specific, or simply undocumented. The result: “it builds on my machine” becomes the team motto. A repeatable, IDE-independent build script — invokable from the command line and runnable inside a CI runner — is non-negotiable for any production codebase.

3. Confusing “Terminate” with “Stop” in AWS

In AWS EC2 terminology, Terminate means permanent deletion of the instance and its associated ephemeral storage — not a graceful process stop. Most operating systems and tools use “terminate” to mean “end a running process,” so developers new to AWS frequently make this assumption and pay for it.

AWS has added safeguards such as termination protection and the DisableApiTermination attribute, but the safest approach is team-wide awareness and access controls. Use IAM policies to restrict termination permissions to only those who need them, and always enable termination protection on instances that hold stateful data.

4. Running Experiments on Production Resources

Closely related to mistake 3: never test an assumption on infrastructure you care about. Even when an operation looks read-only — querying attributes, listing resources, running a diagnostic — validate it on a throwaway environment first. Cloud environments make it cheap to spin up isolated test instances. Use them.

This applies beyond AWS: database migrations, configuration management scripts, and infrastructure-as-code changes should all be validated in a staging environment that mirrors production before they ever touch production.

5. Chasing Absolute Data Integrity at the Cost of Performance

Strong consistency is valuable. Over-indexing on it is expensive. Resorting to SERIALIZABLE isolation levels or explicit table locks to guarantee absolute data integrity is usually a sign of one of three things: a schema that is not modelling the domain correctly, an incomplete understanding of concurrency requirements, or risk aversion that has not been calibrated against actual business needs.

Most applications tolerate eventual consistency or snapshot isolation for the vast majority of their read paths. Understand the actual consistency requirements for each operation and choose the appropriate isolation level deliberately, rather than defaulting to the most restrictive setting.

6. Mixing Code and Markup

Embedding business logic directly in HTML templates — or generating HTML by concatenating strings inside application code — was common practice in the mid-1990s. It remains surprisingly common today, especially in JavaScript-heavy codebases where components grow organically.

The problems are well-documented: poor testability, difficult maintenance, XSS vulnerabilities from naive string concatenation, and an inability to change the presentation layer without touching business logic. Use a proper templating engine, component model, or at minimum a clear separation between data preparation and rendering.

7. Using a List When a Better Data Structure Exists

Lists are the universal fallback. They are fine for prototyping when the shape of the data is still unknown. They become a performance problem when used reflexively in production code instead of the appropriate data structure.

Common misuses include using a list where membership checks are frequent (use a set or hash map instead), performing manual sorting that could be handled by a sorted tree structure, or repeatedly inserting near the middle of an array-backed list (O(n) per insert). Spend five minutes choosing the right collection type before writing the first line of logic — it pays off disproportionately under load.

8. Over-Engineering Class Hierarchies

Deep inheritance trees are one of the most persistent legacies of early OOP education. Building elaborate class hierarchies before the domain is well-understood produces rigid, brittle structures that are hard to refactor and harder to explain to the next developer.

Favour composition over inheritance. Prefer shallow, interface-driven designs. Inheritance is appropriate when you have a genuine is-a relationship and a stable base. It is not the right first instinct for every modelling problem.

9. Taking Functional Purity Too Far

Functional programming offers real benefits: referential transparency, easier testing, and better reasoning about state. But applying functional principles dogmatically — wrapping every operation in nested function compositions, avoiding all mutable state even where it is appropriate, or restructuring code so that adding a feature requires unwinding a chain of a dozen composed functions — creates its own form of complexity.

The goal is maintainable, correct software. Use functional techniques where they simplify the code. Know when a straightforward imperative or object-oriented approach is clearer for the next person reading it.

10. Defaulting to Global Scope

Global state is the path of least resistance and the source of some of the hardest bugs to diagnose: race conditions, unexpected side effects, and state that accumulates across requests or threads. Global variables cannot be parallelised safely without explicit synchronisation, and they make unit testing nearly impossible because tests cannot be isolated from one another.

Apply the principle of least privilege to scope. Pass dependencies explicitly. Use dependency injection or module-scoped singletons with clear lifecycle management instead of bare globals.

11. The “God Object” Anti-Pattern

A class with hundreds of fields and methods that touches every part of the application is a God Object. It violates the single-responsibility principle, makes testing extremely difficult, and tends to get stored in session or application state because it is the only structure available — consuming excessive memory and causing expensive replication in clustered environments.

When you find yourself adding another field to an already large class because it is convenient, take it as a signal to decompose. Build smaller, focused objects with well-defined responsibilities.

12. Death by a Thousand Micro-Objects

The opposite extreme is equally problematic. When every concept in the domain has its own class and none of those classes are longer than a few lines, the cognitive overhead of navigating the codebase becomes prohibitive. Often this pattern co-occurs with over-engineered inheritance hierarchies.

There is no universal correct class size, but if every file is under 30 lines and you have thousands of them, or if following a single operation requires jumping through dozens of delegation layers, the design has been fragmented beyond its useful limit.

13. Writing Custom Threading Logic Unnecessarily

Most business application logic does not require you to manage threads directly. Thread management is genuinely hard: deadlocks, race conditions, and memory visibility issues are notoriously difficult to debug and reproduce. The good news is that modern frameworks and infrastructure — application servers, message queues, reactive frameworks, distributed compute engines like Apache Spark — handle threading and concurrency for you.

Write single-threaded business logic and let the infrastructure layer handle concurrency. Reach for explicit threading only when you are building infrastructure-level components that genuinely require it.

14. Relying on Row-Level Locking

Row-level locking is the default on most relational database platforms, and under light, sequential load it performs well. Under concurrent load, contention on row locks causes queries to queue up and wait, leading to the classic symptom of an application that works fine in testing and mysteriously hangs under production load.

Snapshot isolation (also called multi-version concurrency control, or MVCC) avoids most lock contention by giving each transaction a consistent point-in-time view of the database. PostgreSQL and Oracle use MVCC by default. SQL Server supports it but requires it to be explicitly enabled (READ_COMMITTED_SNAPSHOT). Understand your database’s concurrency model and configure it appropriately rather than accepting defaults uncritically.

15. Using Database Sequences for All Unique Keys

Database sequences and auto-increment columns are a natural first choice for generating unique primary keys. However, they route every insert through a single serialised counter in the database engine — a bottleneck that becomes painful at scale and complicates distributed database architectures and bulk imports.

Unless you genuinely need sequentially ordered keys (which is uncommon for primary keys), use a UUID (version 4, backed by a cryptographically secure random generator). The collision probability of a properly generated UUID is negligible for any realistic data volume. UUIDs can be generated independently on any application node without database coordination, which is a significant scalability advantage.

Final Thoughts

These mistakes are not a mark of incompetence — they are patterns that emerge naturally under deadline pressure, from habits carried over from previous projects, or simply from not yet having encountered the failure mode they produce. The distinguishing mark of a senior engineer is not that they never make these mistakes, but that they recognise them quickly, understand why they are problems, and refactor decisively. Review this list periodically. It is easy to spot these patterns in someone else’s code; the harder skill is spotting them in your own.

0 0 votes
Article Rating
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
0
Would love your thoughts, please comment.x