Development Tools & Frameworks
Software Mile chooses tools and frameworks for what the project needs and who will maintain it – not for what is fashionable this year. The right stack is the one your team can support after we hand it over.
How We Choose a Stack
- Mainstream, well-supported frameworks with real talent pools behind them
- The right tool per layer – front end, back end, data, infrastructure – not one framework forced everywhere
- Attention to maintainability and hiring: can a normal developer pick this up?
- Honest avoidance of lock-in to niche technologies nobody will support in three years
The best technology choice is often the boring, proven one. Tell us your project and we will recommend a stack you can live with.
Judging a Framework Before You Commit
Popularity tells you something real about hiring and about how many libraries exist. It tells you nothing about whether the project will still be maintained in four years, and that is a separate question worth asking on its own terms. How often are major versions released, how disruptive was the last one, is there a long-term support policy, and who funds the maintainers. A project with heavy adoption and one unpaid maintainer carries a risk the download numbers hide.
Then check the ecosystem for the specific things your project needs. Authentication, payments, reporting, file handling, and integration with whatever you already run are where a thin ecosystem hurts. A framework with plenty of tutorials and no mature library for the one thing you must do leaves you writing that library yourself.
When Is Choosing Something Less Mainstream Justified?
It is not never. A newer or narrower technology is defensible when the mainstream option genuinely cannot do the job, when your own team already has real depth in it, or when the component involved is small enough to replace without touching anything else.
What makes it dangerous is placement. Keep an unusual choice at the edge of the system, behind an interface, doing one job. Novelty at the center, in the data model or the framework everything else is written against, is where a team ends up unable to hire and unable to upgrade at the same time.
Does the Stack Decide Whether You Can Hire?
The stack shows up in every job posting you will write. Worth noticing: a popular language paired with an obscure framework can be harder to staff than a less popular language with a standard one, because the framework knowledge does not transfer and the learning curve lands on every new hire.
The question to answer before choosing is who maintains this in three years. Your own developers, a partner, or contract capacity through staff augmentation all point at different answers, and so does where those people are. A stack that is common in the market you actually hire from is worth more than one that is common on the internet.
What Does Framework Debt Look Like Later?
It rarely announces itself as a bad decision. It arrives as small refusals: a security patch you cannot apply because a transitive dependency pins an old version, a build that only works on one machine, a major upgrade nobody will schedule because the breaking-change list is long and the tests are thin.
The practical defense is treating upgrades as routine maintenance with a standing budget, so that no single one has to be justified as a project. Small, frequent upgrades are boring. The upgrade that keeps getting deferred is the one that eventually arrives as a rewrite proposal, which is why automated tests around the parts most likely to break are worth writing early.
- Major versions two or more behind, with no upgrade in anyone’s plan
- Dependencies whose last release predates your project
- Build tooling that is not reproducible from a fresh checkout
- Test coverage too thin to make an upgrade provable rather than hopeful
Should One Stack Be Used Everywhere?
Standardizing has real benefits: developers move between projects, hiring is simpler, and tooling and pipelines are shared. Forcing one framework across problems it does not fit has real costs, usually paid by whoever inherits the awkward fit.
A small approved set, with a written reason required to add to it, is a reasonable middle for an organization running several systems. For a single client with one application the answer is simpler: pick one stack per layer, use it consistently, and document why. Either way the default is the same. The option a competent developer can pick up is usually the right starting point, and the burden of proof sits with anything chosen for other reasons.
The Support Window Underneath the Framework
A framework's support policy sits on top of a runtime policy, and the shorter of the two is the one that sets your real deadline. Node.js even-numbered majors ship in April, enter long-term support that October, and stay supported for 30 months from that point, which puts end of life in April roughly three years after release; odd-numbered majors ship in October, hold Current status for six months, and reach end of life the following June. .NET keys the same distinction to the version number rather than the calendar: even-numbered versions are LTS with three years of support, odd-numbered versions get eighteen months, and both ship in November. Python releases a minor version each October with five years of support, of which roughly the first eighteen months carry bug fixes and the rest is security only. Java has settled into an LTS release every two years, with the intermediate releases getting updates only until the next one appears six months later.
The second-order effect is what catches teams out. Managed platforms deprecate runtimes on a schedule keyed to those upstream dates, and deprecation is usually staged: first you can no longer create new functions or apps on the retired runtime, then updates to existing ones are blocked too. At that point "we will upgrade later" has quietly become "we cannot ship a hotfix," and the upgrade gets done under incident pressure instead of as planned work. The runtime end-of-life date is a fixed date on a calendar somebody else owns. The framework's own major version is usually the softer of the two constraints.
Four dates are worth writing into the project's own documentation on the day the stack is chosen:
- Runtime end of life, and whether the release you picked is the long-term one or the short-term one
- The hosting platform's deprecation date for that runtime, which normally follows upstream end of life rather than leading it
- The framework's support policy for the major version you are on, and whether security fixes are backported to it at all
- The database major version's end of life, since managed database services schedule forced upgrades against it
The Deployment Target Constrains the Framework Before You Do
Deciding where the system runs before deciding what it is written in removes options that would otherwise look reasonable on paper. A request-scoped, scale-to-zero runtime has no persistent process, so everything a framework normally does in-process between requests stops working: an in-memory cache is per-instance and usually cold, an in-process scheduler fires once per live instance or not at all, and long-lived connections such as WebSockets, server-sent events, or database notification channels have nowhere to live. The frameworks that assume a long-running server are not wrong. They are being asked to run on a shape they were not written for, and the workarounds land in your code, not theirs.
Connection handling is where this surfaces first. Every concurrent instance opens its own database connections, and a managed Postgres instance has a fixed max_connections that scales with instance size, so a workload that scales out horizontally exhausts connections long before it exhausts CPU. A pooler in front of the database is the standard answer, and transaction-mode pooling then takes away session-scoped behavior: protocol-level prepared statements (unless the pooler version tracks them), LISTEN/NOTIFY, session-level advisory locks, and session-scoped temporary tables all stop being reliable, because consecutive statements from one client can land on different backend connections. Several drivers and ORMs issue prepared statements by default and have to be configured out of it, which is a framework-level decision made by an infrastructure constraint.
Settle these before shortlisting frameworks, because each one eliminates options rather than ranking them:
- Where it runs: long-running containers, a managed application platform, or per-request functions
- Whether the application may hold state in memory or on local disk between requests
- How background work runs: in-process scheduler, external queue with dedicated workers, or platform cron
- Whether the application reaches the database over a private network or the public internet, and what that does to connection setup cost and TLS handling
- Whether outbound calls have to leave from a fixed address because a third-party vendor allowlists it
What Reproducible From a Fresh Checkout Requires
Reproducible means a clean clone, on a machine that has never built the project, produces the same artifact as the machine that built it last week. Four inputs have to be pinned for that to hold. The first is the resolved dependency graph rather than the declared ranges: a manifest entry of ^4.2.0 resolves to whatever is newest at install time, so the lockfile is the actual input to the build. It has to be committed, and CI has to install from it strictly. The npm ci behavior of refusing to run when the lockfile and manifest disagree, and of deleting the existing dependency directory first, is the point of the command and not an inconvenience to work around.
The other three are the toolchain, the base image, and anything compiled during install. Toolchain means the language runtime, the package manager, and any compiler, each at a specific version, because a package manager major version can change how the graph resolves. Base image means a digest, not a tag: node:20 moves under you, sha256:... does not. Install-time compilation is the one people forget, since native modules build against the runtime's ABI version and against whatever C++ toolchain and headers the build machine happens to have, so the same lockfile can produce different binaries on two machines. A verification build that exercises all four looks like this:
- Build inside a container started from a base image pinned by digest, not by tag
- Delete the dependency directory and the package manager cache, then install from the lockfile alone
- Commit the lockfile change in the same commit as the manifest change, without exception
- Record the package manager version in the repository and have CI use exactly that one, through the packageManager field or the equivalent for your ecosystem
License Terms Change at a Version Boundary
A relicensing does not reach backward. The version you already hold stays under the terms it was released under, and every fix and feature published after the change arrives under the new ones, which means you can stay put but you stay put without patches. Source-available licenses are the common form this takes now. The Business Source License, for example, sets a change date no more than four years after a release, on which that release converts to a named open source change license, plus an additional use grant describing what production use is permitted before then. In the interim it is not open source, and the restriction is usually aimed at offering the software as a competing hosted service, which most application teams are not doing. That is still a reading your counsel does rather than your developers.
Copyleft obligations differ in what triggers them, and the trigger matters more than the license family. The AGPL requires that users interacting with a modified version over a network be offered the corresponding source, so network use is enough and distribution is not required, which makes an AGPL component inside a hosted product a different question from a GPL one and a different question again from linking a permissively licensed library. Automated license scanning helps but proves less than it appears to: the identifier in a package manifest is self-declared, and it can be absent, stale, or in disagreement with the LICENSE file in the source tree, so a clean report is evidence rather than proof.
Keeping an unusual component at the edge of the system, behind an interface, pays off a second time when its terms change: it is the easiest kind of component to swap, and the swap does not require agreement from the rest of the codebase.
Frequently Asked Questions
Is this a service on its own, or part of building something?
It is the decision phase of a build, and it is also done as a review of a stack you already run. It is not procurement: no licenses are resold and no hosting is supplied under it. What it produces is a choice per layer with the reasoning written down, along with the version and support constraints that come attached to each choice. Those constraints are the part that survives, because they are what the next upgrade decision gets argued against.
What actually decides which stack is right?
Constraints that eliminate options decide it, not preferences that rank them. The main ones are the expected life of the system measured against the support windows of the runtimes available today, the deployment target and whether it permits a long-running process, the shape of the data and whether the workload needs transactions spanning several records, and any data residency rule that limits which regions and which managed services are usable at all. Preferences among whatever survives that filter still matter, but they matter last and they are the easiest part to change.
What has to already exist before a stack can be chosen?
Your identity provider, the database you already operate, your CI system, your cloud account with its network boundaries, and the deployment target. These narrow the field before anyone argues about frameworks, because they constrain the choice through the client libraries, drivers, and deployment shapes they support. If none of it exists yet, then the cloud account and the identity decision come first, since almost everything downstream inherits from them.
Can we keep the stack we already have?
Often yes. A stack two majors behind with a supported upgrade path is a maintenance problem: the work is bounded, the release notes exist, and each step can be verified before the next one starts. A runtime past end of life with no upgrade path, or a framework whose maintainers have stopped publishing, is a replacement problem, and treating it as maintenance stretches it out without ever finishing it. The quickest diagnostic is to attempt the upgrade on a branch and see how far the build gets before it stops, because that tells you which of the two situations you are in.
How does the work actually proceed?
In order: inventory the systems the new work has to talk to and the constraints they impose, fix the deployment target, pick the runtime by support window, then choose per layer. After that, prove the choice with one thin vertical slice that runs from the interface through the API to the real database in the real deployment target, including CI and an actual deploy, before any features are written. That slice is where driver incompatibilities, connection limits, and authentication mismatches show up, and it finds them while changing the decision is still only a decision.
What do we have to provide or decide?
Access and ownership, mostly. The repository and CI should sit in your organization's own accounts, the cloud account should be in your organization's name rather than a vendor's, you need DNS control, and each system that has to be integrated needs either credentials or a sandbox. Two decisions are genuinely yours: the hosting region, and whether source-available or copyleft terms are acceptable anywhere in the dependency graph. The second one rules specific databases and libraries out before anyone evaluates them on technical grounds, so it is better answered early than discovered late.
Should lockfiles be committed?
For applications, yes. The lockfile is the real build input, it belongs in the repository, and CI should install from it strictly rather than resolving fresh. For a library published to a registry the situation is different, because consumers resolve their own graph from your declared ranges and never see your lockfile; guidance varies by ecosystem on whether to commit one anyway for CI stability. Either way, a library's CI should also run against the newest versions its ranges allow, since those ranges, not the lockfile, are what consumers actually get.