A language choice is a hiring choice, a hosting choice and a support obligation rolled together. Below is what we run in client production, the situation in which each item earns its place, and our reservations about it.
The back end owes more than endpoints returning rows: correctness under concurrency, a defensible account of last night's invoice batch, behaviour when a payment gateway is down, an audit trail that survives scrutiny. Little of that depends on language, and interfaces vary too: REST, queues, and still many scheduled file drops between Australian platforms. Most systems touch four or five of the items below.
Default for data-heavy services, integration work and report generation, for dependable enterprise client libraries and readable code. Dependencies are pinned and builds locked; CPU-bound work belongs in a worker, not requests.
Default where Microsoft infrastructure exists already, especially alongside SQL Server, directory services or a line-of-business suite. Strong tooling, predictable upgrade cadence, most economical on a Windows-centric estate.
Used for integration hubs and services orchestrating other systems. Shared types with the front end remove a category of defect, and async input suits the workload. It serves CPU-heavy work poorly; versions are pinned and audited.
Retained because much mid-market software runs on it; current versions are productive and cheap to host. Older procedural estates we stabilise, test around risky paths and migrate in slices.
Reserved for real performance requirements: stream consumers, high-volume parsers, services that must start fast and sit quietly. Restricted deliberately, because few clients hire for it locally, so each component must be readable alone.
Suits systems with a substantial data model, an administration requirement and layered permissions, all arriving maintained. The cost is convention: building against the grain grows painful, so it rewards following its idiom.
The choice where the contract is the deliverable and typed validation matters. Schemas generate accurately and throughput is good. Domain logic stays outside route functions, since effortless handlers tempt teams to embed the domain there.
Baseline where .NET runs in Linux containers as well as Windows hosts, with injection, configuration and structured logging consistent throughout. Its weakness is ceremony: small services carry scaffolding warranted at enterprise scale.
How we extend or replace PHP systems clients already rely on, since queues, scheduling and authentication are coherent there. We avoid its elaborate magic and keep business rules in plain classes, because debugging through convention is what ruins inherited PHP.
The default interface style, versioned from the first release and documented from the server rather than a wiki. Predictable beats clever when consumers are other companies' software, and the discipline demanded is deprecation.
Used when several clients need different views of one dataset and the alternative is bespoke endpoints everywhere. It removes round trips and gives consumers control, which is the risk: without depth limits, one can saturate the database.
Anything that must not be lost, whether invoice posting or ERP updates, travels with retries, backoff and a dead-letter destination, on managed cloud queues by default. Every queue needs someone named for the day it backs up.
Four comparisons that arise in nearly every scoping conversation, each with conditions attached.
Choose it when the caller needs an immediate answer and work completes in under a second. Avoid it where a downstream system is unreliable, because a struggling ERP becomes a failing user journey. Work acknowledged now and finished later belongs elsewhere.
Choose it when a message must survive an outage or several consumers need the same fact. Avoid it where nobody monitors the queues, since an ignored dead-letter mailbox is a slower way to lose data. Ordering then becomes your problem.
Choose it almost always at the start: one application with clear internal boundaries can be deployed by one person and read end to end. Avoid splitting for its own sake, since distributed transactions without a budget produce unreconcilable inconsistency.
Choose it when a defined component has requirements the primary language cannot meet, such as sustained throughput or a minute memory footprint. Avoid it for preference: two patch cycles, two toolchains, fewer people able to fix either.
Observations from systems we operate rather than vendor documentation. Each cost somebody real hours before becoming a rule.
A function-per-request design opens a connection per concurrent invocation, so a modest spike exhausts a small instance quickly. Where serverless is needed, a pooler sits in front of the database.
Lazy loading turns one action into hundreds of queries once a list passes twenty rows. Query logging runs in development, an unexpected count is a defect in review, and related data is fetched explicitly.
An ERP recovering from an outage receives every retry at once, finishing what the outage started. Retries are capped, jittered and backed off, and a breaker trips before a failing dependency takes the caller too.
Scheduling across Perth, Kalgoorlie and eastern sites creates real ambiguity around daylight saving and end-of-day. Everything is stored in UTC and rendered in AWST at the edges, or a day's transactions reconcile against the wrong window.
An export returning every row works for a year, then the data doubles and requests time out at the balancer. List endpoints are paginated from the first release, and bulk exports run asynchronously.
A practice that accepts every technology is undisciplined rather than skilled. These positions follow from consequences we have watched clients absorb.
We have evaluated languages that suit certain problems well and declined to build client systems on them, because clients could not hire for them locally and would depend on us by default. Accidental dependence is a poor outcome.
We do not write our own object mapper, injection container or application framework. Each exists already, maintained by people doing it full time; a bespoke version becomes knowledge held by exactly one person.
We rarely recommend replacing functioning software in one programme. Rewrites discover undocumented behaviour during their costliest phase, and the business pays twice for capability it already had.
We do not deploy onto versions no longer receiving security patches, and say so in writing when asked to accept one. That trade shortens a timeline by weeks and lengthens exposure by years.
We assess existing server-side systems before proposing anything. That conversation usually changes the scope.