Technology · Backend

Server-side decisions made for whoever maintains them next

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.

What the server side is accountable for

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.

Primary languages

5 technologies
Python
C# & .NET
TypeScript & Node.js
PHP
Go

Python

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.

C# and .NET

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.

TypeScript and Node.js

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.

PHP

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.

Go

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.

Frameworks we put in production

4 technologies
Django
FastAPI
ASP.NET Core
Laravel

Django

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.

FastAPI

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.

ASP.NET Core

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.

Laravel

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.

Interfaces, messaging and batch work

3 technologies
REST & OpenAPI
GraphQL
Queues & workers

REST with OpenAPI

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.

GraphQL

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.

Queues and background workers

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.

Choose it when, avoid it when

Four comparisons that arise in nearly every scoping conversation, each with conditions attached.

Synchronous request and response

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.

Event-driven messaging

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.

One deployable application

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.

A second language in one estate

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.

Practical notes from production

Observations from systems we operate rather than vendor documentation. Each cost somebody real hours before becoming a rule.

01

Functions exhaust connection pools

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.

02

Object mappers generate N+1 queries quietly

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.

03

Retries attack your own dependencies

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.

04

Western Australian time is a data problem

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.

05

Unbounded endpoints fail eventually

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.

What we deliberately do not use

A practice that accepts every technology is undisciplined rather than skilled. These positions follow from consequences we have watched clients absorb.

Languages we cannot staff locally

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.

Bespoke frameworks and home-grown mappers

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.

Wholesale rewrites of working systems

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.

Unsupported runtimes, even briefly

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.

Review the backend you already run

We assess existing server-side systems before proposing anything. That conversation usually changes the scope.

Get a Quote