Avoiding overkill: embracing simplicity
A contact form implemented with React, Redux, Webpack, TypeScript, and elaborate CI/CD pipelines—2.3MB production bundle for three fields and a submit button. Two days to set up the development environment. Thirty-five minutes to change placeholder text. This is overengineering: enterprise solutions applied to problems that need HTML and a server script.I once reviewed a codebase for a contact form—a web page where users submitted their name, email, and message. The implementation used React for the frontend, Redux for state management, Webpack with elaborate configuration, Babel for transpilation, ESLint with custom rules, Prettier for formatting, Jest with extensive mocking, TypeScript with strict mode, and deployed through a CI/CD pipeline with automated testing gates and staging environments. The production bundle was 2.3MB. The form had three fields and a submit button.
The original developer had left the company. The new maintainer spent two days setting up the development environment—Node version managers, build tool configuration, environment variable setup, obtaining API keys for third-party services the application didn't actually use. When a client requested changing the placeholder text on one field, the modification took twenty minutes. The rebuild and redeployment took another fifteen. For changing five words in a text string.
This is overengineering in its purest form: applying enterprise-scale solutions to problems that don't warrant them. The contact form could have been a single HTML file with inline JavaScript and a server-side script to handle submission. Development time measured in minutes. Deployment via FTP. Maintenance requiring basic text editing. But somewhere in the project's history, someone had decided this contact form needed to be "production-ready", "scalable", and "maintainable", and those aspirations manifested as a technology stack that would be reasonable for a Fortune 500 company's customer portal but was catastrophically inappropriate for three form fields.
Research from the Software Engineering Institute found that over 80% of software projects suffer from some form of overengineering, leading to increased development time, maintenance costs, and technical debt.1 The pattern repeats constantly: simple requirements met with complex solutions. The complexity creates its own maintenance burden—dependencies to update, frameworks to migrate when versions change, configuration files that become archaeology projects when someone needs to modify behaviour, and documentation that's perpetually out of date because writing it is harder than writing the code itself.
Matching solutions to problems
The appropriate level of complexity depends on the problem's actual requirements, not imagined future needs. "We might need to scale this later" justifies precisely zero additional complexity until scaling becomes a demonstrated need rather than speculation. "This could become more complicated" doesn't warrant building complicated infrastructure preemptively. The goal is solving the current problem with the simplest approach that works, then adding complexity only when requirements demand it.
Kevlin Henney frames this elegantly: "There are two ways of constructing a software design: One way is to make it so simple that there are obviously no deficiencies, and the other way is to make it so complicated that there are no obvious deficiencies."2 The first approach reveals problems immediately because they stand out against simplicity. The second approach hides problems behind complexity where they fester until they become critical failures.
Consider file management automation. Building a custom file manager application with a GUI, database backend, plugin architecture, and API seems comprehensive. But the actual requirement—batch renaming files, compressing directories, synchronizing folders—is solved completely by combining macOS Automator workflows with shell scripts. The shell script approach takes hours to implement, requires no dependencies, breaks in obvious ways when problems occur, and any developer can modify it with basic knowledge. The custom application takes weeks, introduces dependencies that need maintenance, fails in mysterious ways when libraries change, and requires developers familiar with its specific architecture.
Data transformation provides another clear example. Converting CSV files to JSON or cleaning spreadsheet data suggests reaching for pandas, Apache Spark, or complete data engineering pipelines with Kafka and orchestration. But the requirement is transforming files, not building data infrastructure. A Python script using the standard library handles CSV to JSON conversion in twenty lines. For many transformations, command-line tools like Miller or jq solve the problem in a single pipeline. The difference in complexity is vast whilst the difference in capability for the actual requirement is zero.
Web development suffers particularly from complexity inflation. Small to medium sites—content-focused blogs, documentation, company websites, product pages—default to full-stack frameworks like React or Angular with elaborate build pipelines, state management libraries, component frameworks, and deployment complexity. Static site generators like Jekyll, Hugo, or Astro produce identical results with a fraction of the complexity. The generated output is pure HTML and CSS—no JavaScript runtime required, no hydration overhead, no client-side routing complexity. For many sites, this is sufficient. When it isn't, adding interactivity to specific components is straightforward. But starting with the complex stack because "we might need it" creates maintenance burden before any actual need materialises.
The pattern holds across domains. API development doesn't require microservices architecture when a simple server-side function suffices. Desktop applications don't need Electron's web stack when native frameworks provide better performance and smaller binaries. Background jobs don't need queue systems and workers when cron and shell scripts handle the volume. Database schemas don't need elaborate normalisation when denormalised structures solve the access patterns. Each additional abstraction layer, framework, and tool adds cognitive overhead, maintenance burden, and potential failure points.
The dependency problem
Every framework and library introduces dependencies. Dependencies require updates. Updates break backwards compatibility. Breaking changes require code modifications. Code modifications introduce bugs. Bugs require fixes. Fixes require testing. Testing requires infrastructure. Infrastructure requires maintenance. The cycle compounds.
Snyk's research found that 79% of open-source projects contain at least one known vulnerability, with the average project containing 75 vulnerabilities.3 These aren't theoretical risks—they're exploitable security holes that attackers actively target. Each dependency multiplies the attack surface. That contact form with 2.3MB of JavaScript includes hundreds of dependencies, each potentially containing vulnerabilities. The simple HTML version has zero dependencies and zero vulnerabilities from third-party code.
The security implications extend beyond known vulnerabilities. Supply chain attacks target popular packages, injecting malicious code that propagates to every project using that dependency. The more dependencies your project includes, the larger the surface area for these attacks. The simpler your solution, the smaller the attack surface.
Beyond security, dependencies create maintenance burden. Framework migrations become projects unto themselves. React 16 to React 17, relatively smooth. React 17 to React 18, concurrent rendering changes required substantial refactoring for many applications. These migrations deliver zero user-visible value whilst consuming engineering time that could address actual features or bugs. The HTML form requires no migration. It works the same way today as it did when written.
Framework lock-in creates another cost. Your codebase becomes fluent in the framework's abstractions but opaque to developers unfamiliar with those conventions. Knowledge becomes tribal. Documentation references framework concepts that make sense within that ecosystem but are incomprehensible to outsiders. The barrier to contribution rises. The pool of developers who can work effectively on the codebase shrinks.
Alan Kay's advice applies perfectly: "Make the simple things simple, and the complex things possible." The framework approach makes simple things complex whilst claiming to make complex things possible. The reality is that when genuinely complex requirements emerge, frameworks often become obstacles rather than enablers. You fight the framework's assumptions about how things should work. You contort your solution to fit the framework's patterns. You discover the framework's flexibility ends exactly where your requirements begin.
Choosing appropriate complexity
The decision process for solution complexity should start with the problem's concrete requirements, not the solution space's possibilities. What does this system need to accomplish? What are the actual constraints—performance, scale, reliability, maintainability? What resources are available for ongoing maintenance? These questions reveal the appropriate complexity level.
Prototype multiple approaches. The shell script solution, the simple web framework, the complex enterprise stack—try them all at small scale. Measure the actual effort required for implementation, deployment, and maintenance. Compare not just initial development time but the ongoing cost of modifications, updates, and debugging. The simplest solution that meets requirements almost always wins this comparison.
Resist the temptation to add complexity "just in case." Future requirements are speculation. Building for speculative futures creates systems that are complex today to solve problems that may never materialise. When those future requirements do arrive, they'll likely differ from what was anticipated, rendering the preemptive complexity useless whilst making the system harder to modify to meet the actual needs.
Stack Overflow's survey data shows that developers who prioritise simplicity and maintainability over adopting the latest features report higher job satisfaction and lower burnout rates.4 This correlation isn't coincidental. Simple systems are comprehensible. Comprehensible systems are maintainable. Maintainable systems don't create the constant crisis mode that leads to burnout. The developer who built that contact form with React, Redux, and elaborate deployment pipelines probably spent weeks fighting build configurations and debugging mysterious state management issues. The developer who could have built it with HTML and a server script would have finished in an afternoon and moved on to actually interesting problems.
That 2.3MB contact form serves as a warning rather than an aberration. The pattern repeats constantly across the industry—simple problems solved with complex infrastructure because complexity feels professional, because frameworks promise safety, because "this is how real applications are built." But real applications are built to solve problems, and the simplest solution that solves the problem is almost always the correct choice.
The Standish Group's research found that 84% of software features are rarely or never used.5 This statistic reflects the disconnect between developer enthusiasm for building comprehensive systems and user needs for functional tools. Users don't care about your technology stack. They don't care whether you used React or vanilla JavaScript. They care whether the form works, whether it loads quickly, whether it's reliable. The simple HTML form meets all these criteria whilst the complex React form creates maintenance burden that diverts resources from actual user needs.
Skill in software engineering isn't demonstrated by knowing every framework or deploying elaborate architectures. It's demonstrated by selecting appropriate tools for each problem, understanding when simplicity suffices and when complexity is justified, and maintaining the discipline to resist overengineering when it offers no tangible benefit. In an industry where complexity is often mistaken for sophistication, choosing simplicity is an act of engineering maturity.
The next time you start a project, resist reaching for the familiar complex stack. Ask whether a simpler approach might work. Prototype the simple version. Measure the actual cost difference. You'll likely find that the simple solution is faster to build, easier to maintain, more secure, and more reliable. And when requirements do demand complexity, you can add it incrementally, building only what you need rather than what you imagined you might need. That's engineering. Everything else is just resume building.
Footnotes
-
Delange, J., McHale, J., Hudak, J., Nichols, B., & Nam, M. (2015). "Evaluating and Mitigating the Impact of Complexity in Software Models." Software Engineering Institute, Carnegie Mellon University, Technical Report CMU/SEI-2015-TR-013. ↩
-
Henney, K. (2010). 97 Things Every Programmer Should Know. O'Reilly Media. ↩
-
Snyk. (2022). The State of Open Source Security. ↩
-
Stack Overflow. (2022). Developer Survey. ↩
-
Standish Group. (2015). CHAOS Report. ↩
Published on:
Updated on:
Reading time:
10 min read
Article counts:
32 paragraphs, 1,808 words
Topics
TL;DR
Research shows 80% of software projects suffer from overengineering—applying enterprise-scale solutions to problems requiring basic implementations. Simple contact forms get React stacks with hundreds of dependencies. File transformations trigger data engineering pipelines. Static sites deploy full JavaScript frameworks. Each dependency multiplies security vulnerabilities (79% of open-source projects contain exploitable holes), creates maintenance burden through framework migrations that deliver zero user value, and introduces complexity that obscures rather than solves problems. The Standish Group found 84% of features go unused, revealing the disconnect between developer enthusiasm for comprehensive systems and user needs for functional tools. Appropriate complexity starts with concrete requirements, not imagined futures. Prototype multiple approaches. Measure actual costs. The simplest solution meeting requirements almost always wins. Users care whether systems work, load quickly, and stay reliable—not about technology stacks. Engineering skill means selecting appropriate tools and resisting overengineering when complexity offers no tangible benefit.