Skip to content
Starone

Capability

Backend systems that stay correct under load and partial failure

A backend is judged by what happens when a dependency times out, a message arrives twice or traffic triples. We design APIs, services and data stores around those conditions: explicit contracts, idempotent operations, bounded queues and storage chosen for the workload.

Overview

Backend engineering is the design of the services, APIs and data stores that hold a system's state and enforce its rules. The difficult part is rarely the happy path. It is the behaviour under concurrency, where two requests modify the same record; under partial failure, where a call may have succeeded, failed or still be running; and under growth, where a query that was fast on a small table becomes a full scan on a large one.

Once a system spans more than one process, the network sits between every component and there are no distributed transactions for free. Calls time out without revealing their outcome, messages are delivered at least once, and replicas lag. Correctness then depends on patterns applied deliberately: idempotency keys, retries with backoff and jitter, circuit breakers, the transactional outbox, sagas with compensating actions, and an explicit decision about which data must be strongly consistent and which may converge.

We build backends in Java with Spring Boot, Go, Node.js and Python, on PostgreSQL, MySQL, MongoDB, Redis, Kafka and OpenSearch, selecting each for the problem in front of it. Architecture starts from access patterns, consistency requirements and failure modes, and a modular monolith is preferred until there is a concrete reason to distribute.

What we engineer

Technical areas

01

API architecture

REST over HTTP suits public and partner APIs because it is cacheable, widely understood and easy to secure at a gateway. GraphQL suits clients with varied data needs over a connected domain, and gRPC suits internal service-to-service calls that need strict contracts, streaming and low overhead. Contracts are written first, in OpenAPI or Protocol Buffers, and breaking changes are detected in CI.

  • REST with OpenAPI contracts
  • GraphQL with persisted queries and complexity limits
  • gRPC and Protocol Buffers
  • API gateways and request routing
  • Cursor-based pagination
  • Additive versioning and deprecation policy
  • Webhooks with signed payloads
02

Authentication, authorisation and traffic control

Authentication establishes who is calling, through OAuth 2.0 and OpenID Connect for users and mutual TLS or workload identity for services. Authorisation is checked on every request at the object level, because a valid token for one tenant must never read another tenant's record. Rate limits and quotas protect capacity and are applied per client, per tenant and per endpoint.

  • OAuth 2.0 and OpenID Connect
  • Short-lived JWTs and token introspection
  • Mutual TLS between services
  • RBAC and ABAC with policy engines such as OPA
  • Token-bucket and sliding-window rate limiting
  • Load shedding and request prioritisation
03

Asynchronous processing and event streaming

Work that does not need to finish inside the request is moved to queues and streams, which absorbs bursts and isolates slow dependencies. Kafka provides a durable, partitioned, replayable log with ordering per key; queue brokers such as RabbitMQ provide per-message routing and acknowledgement. Consumers are written to be idempotent because delivery is at least once.

  • Apache Kafka topics, partitions and consumer groups
  • RabbitMQ and cloud queue services
  • Redis Streams for lightweight pipelines
  • Dead-letter queues and replay tooling
  • Schema registry with Avro or Protobuf
  • Backpressure and consumer lag control
04

Resilience and consistency patterns

Every remote call has a timeout, a bounded retry policy with exponential backoff and jitter, and a circuit breaker that stops calls to a failing dependency. Operations with side effects accept an idempotency key so that a retry cannot charge, ship or notify twice. Multi-service workflows use sagas and the outbox pattern instead of two-phase commit.

  • Idempotency keys and deduplication stores
  • Retries with exponential backoff and jitter
  • Circuit breakers and bulkheads
  • Transactional outbox with change data capture
  • Sagas: orchestration and choreography
  • Optimistic locking and version columns
05

Data stores and caching

A relational database is the default system of record; other stores are added when a specific access pattern justifies them. Redis provides distributed caching, rate-limit counters, locks with expiry and session storage. Search engines and vector indexes are derived views populated from the system of record, never the source of truth.

  • PostgreSQL and MySQL
  • MongoDB
  • Redis
  • Elasticsearch and OpenSearch
  • Vector search with pgvector, Qdrant or Milvus
  • Cache-aside, write-through and TTL strategies
  • Read replicas, partitioning and sharding
06

Languages and runtime engineering

Java with Spring Boot is chosen for large domain-heavy systems that benefit from a mature ecosystem, strong typing and virtual threads for blocking I/O at scale. Go suits network services and infrastructure components with predictable latency and small static binaries. Node.js fits I/O-bound APIs and teams sharing TypeScript with the frontend, and Python fits data-intensive and machine learning services.

  • Java and Spring Boot
  • Go
  • Node.js with TypeScript and NestJS
  • Python with FastAPI
  • Connection pooling and thread or event-loop tuning
  • Profiling, heap analysis and GC tuning

Choosing a database by workload, not by trend

The right database follows from four properties of the workload: the access patterns, the consistency the business rules require, the write volume and the shape of the queries. Listing the top queries with their expected frequency, cardinality and latency budget, before choosing a product, removes most of the debate. A system that needs multi-row transactions, joins and ad hoc queries over data of moderate size belongs in PostgreSQL or MySQL, and that describes most business applications.

Specialised stores earn their place through a pattern the relational database serves poorly. Each one also adds an operational surface: backups, upgrades, monitoring, failover and a second copy of data that must be kept in sync. PostgreSQL with JSONB, full-text search, partitioning and pgvector covers a wide range before a second system is justified, so the burden of proof sits with the addition.

  • PostgreSQL or MySQL: transactional systems of record, relational integrity, mixed read and write workloads, reporting through replicas
  • MongoDB: aggregates that are read and written as whole documents with variable structure, where cross-document transactions are the exception
  • Redis: sub-millisecond key access for caches, counters, sessions, leaderboards and short-lived coordination, with persistence treated as a convenience
  • Elasticsearch or OpenSearch: full-text relevance, faceting and log analytics over an inverted index, fed asynchronously and tolerant of refresh delay
  • Vector databases: approximate nearest-neighbour search over embeddings with HNSW or IVF indexes, trading recall for latency and memory
  • Kafka: an ordered, replayable event log between systems; it is a transport and a buffer, not a query engine

Architecture

Event-driven architecture

Producers

  1. 01

    Command

    Service handles a request

  2. 02

    Local transaction

    State and outbox row commit together

  3. 03

    Outbox relay

    Polls or tails the log, then publishes

  4. 04

    Publish

    Keyed event with schema version

Broker

  1. 01

    Topic

    One per event type or aggregate

  2. 02

    Partitions

    Ordering holds per key

  3. 03

    Retention

    Replay from an earlier offset

  4. 04

    Dead-letter queue

    Holds events that keep failing

Consumers

  1. 01

    Consumer group

    Scales out across partitions

  2. 02

    Idempotent handler

    Dedupe on event ID before applying

  3. 03

    Retry with backoff

    For transient faults only

  4. 04

    Commit offset

    After the side effect succeeds

  5. 05

    Project state

    Update read models and caches

Producers publish events through a transactional outbox, the broker preserves order per key, and consumers apply each event idempotently. Events that cannot be processed are parked in a dead-letter queue instead of blocking the partition.

Consistency across services: outbox, sagas and idempotency

The dual-write problem appears whenever a service updates its database and publishes a message. If the two operations are separate, a crash between them leaves the database changed and the event unpublished, or the reverse. The transactional outbox removes the gap: the event is inserted into an outbox table in the same local transaction as the state change, and a relay, commonly change data capture with Debezium, publishes it to Kafka afterwards. Delivery becomes at least once, which is why consumers must deduplicate.

A business process that spans services, such as reserve stock, take payment and create shipment, cannot hold one ACID transaction across them. A saga models it as a sequence of local transactions, each with a compensating action that semantically undoes it. Orchestration puts the sequence in one coordinator, built on a workflow engine such as Temporal or a state machine in the database, which makes the process visible and testable. Choreography lets services react to each other's events, which reduces coupling for short flows and becomes hard to reason about for long ones.

Idempotency makes retries safe. The client sends a unique key with each mutating request; the server stores the key with the result inside the same transaction as the effect and returns the stored result when the key is seen again. Combined with deadlines propagated through the call chain and a retry budget that prevents retry storms, this lets the system recover from timeouts without duplicating side effects.

Synchronous calls, queues and event streams

A synchronous call couples the availability and latency of the caller to the callee: if a request path crosses five services, its availability is the product of all five. It remains the right choice when the caller needs the answer to continue, for example a balance check before authorising a payment. Everything that can happen after the response, such as notifications, projections, search indexing and analytics, is better driven by events.

Queues and streams differ in what they retain. A queue hands each message to one consumer and removes it on acknowledgement, which suits task distribution with per-message retry. A Kafka topic retains the log for a configured period regardless of consumption, so new consumers can replay history, several consumer groups can read independently and ordering is guaranteed within a partition. The partition key therefore determines both ordering and parallelism, and choosing it is a design decision, typically the aggregate identifier such as account or order.

Eventual consistency is a product decision as much as a technical one. Read models updated from events lag the write side, so the interface must either read its own writes from the source, show pending state or tolerate the delay. We document the expected lag for each projection and alert when consumer lag exceeds it.

Distributed caching without serving wrong data

A cache is a second copy of data with its own consistency problem. Cache-aside with a time to live is the simplest correct pattern: read from Redis, fall back to the database on a miss, write the result with an expiry, and delete the key when the underlying row changes. Deleting on write is safer than updating, because concurrent writers can otherwise leave the older value in place.

The failure modes are predictable and need explicit handling. A stampede occurs when a popular key expires and many requests recompute it at once, addressed with request coalescing, probabilistic early refresh or a short lock. Hot keys concentrate load on one shard, and an unbounded key space exhausts memory unless eviction policy and key cardinality are planned. The service must also remain correct, if slower, when the cache is unavailable.

How we engineer it

Our approach

We design the backend from its data and failure modes outwards, and keep the number of moving parts as low as the requirements allow.

  1. 01

    Model the domain and its invariants

    We identify aggregates, the rules that must hold transactionally and the boundaries across which eventual consistency is acceptable. These boundaries, not team preference, decide where a service split is possible later.

  2. 02

    Characterise the workload

    Access patterns, read-to-write ratio, data volume, growth rate and latency objectives are written down per operation. This drives schema design, indexing, data-store selection and capacity planning.

  3. 03

    Define contracts first

    OpenAPI, GraphQL or Protobuf schemas and event schemas are agreed before implementation, including error models, pagination, idempotency behaviour and versioning rules. Consumers build against mocks generated from the same contracts.

  4. 04

    Implement with failure handling built in

    Timeouts, retries, circuit breakers, outbox publication and idempotent consumers are part of the first implementation, not a hardening phase. Schema changes use expand-and-contract migrations so that old and new code run side by side.

  5. 05

    Test against realistic conditions

    Integration tests run against real databases and brokers in containers, contract tests guard service boundaries, and load tests use production-shaped data volumes. Fault injection verifies behaviour when a dependency is slow, unavailable or returns duplicates.

  6. 06

    Operate against service level objectives

    Each service has defined objectives for availability and latency, dashboards for the four golden signals, alerts tied to error-budget burn and runbooks for its known failure modes.

Trade-offs

Decisions that shape the design

The questions we work through with your team before committing to an architecture.

REST, GraphQL or gRPC?
Use REST for public and partner APIs where HTTP caching, tooling and broad client support matter. Use GraphQL when many client screens need different projections of a connected graph and you can invest in query cost limits, batching against N+1 queries and field-level authorisation. Use gRPC between internal services for typed contracts, deadlines and streaming; browsers need a gRPC-Web proxy, so it is rarely the right external interface.
Modular monolith or microservices?
Start with a modular monolith when one team owns the domain: modules with enforced boundaries and separate schemas give most of the design benefit with one deployment and local transactions. Extract a service when a module needs independent scaling, a different release cadence, a different runtime or isolation of failure. Microservices exchange in-process calls for network calls, so they are justified by organisational and operational needs, not by code tidiness.
Kafka, a message queue or Redis Streams?
Choose Kafka when events must be retained and replayed, consumed by several independent groups and ordered per key at high throughput. Choose RabbitMQ or a managed queue for task distribution with per-message acknowledgement, delay and routing, where history is not needed. Redis Streams is adequate for modest volumes when Redis is already in place and losing a small window of data on failover is tolerable.
Where do we need strong consistency, and where is eventual consistency acceptable?
Keep invariants that cannot be violated even briefly, such as balances, inventory decrements and uniqueness, inside one database transaction in one service. Accept eventual consistency for derived data: search indexes, read models, notifications and analytics. If an invariant spans two services, that is evidence the boundary is in the wrong place.
Should we add a second database?
Only when a measured access pattern cannot be met by the primary store with appropriate indexing, partitioning or replicas. Search relevance, high-dimensional similarity search and very high write throughput on simple keys are legitimate reasons. Each added store requires a synchronisation path, usually change data capture, and a plan for rebuilding it from the system of record.
Which language and framework should the backend use?
Choose by the team's operating experience first and the workload second. Java with Spring Boot fits complex transactional domains and large teams; Go fits high-concurrency network services with tight memory and start-up budgets; Node.js fits I/O-bound APIs and is a poor fit for CPU-heavy work on the event loop; Python fits services close to data and model code. Limiting the organisation to two or three runtimes keeps libraries, pipelines and on-call knowledge shared.

Production readiness

What makes it production-ready

How it is secured

Requests are authenticated at the gateway and authorised again in the service at object level, with tenant identity carried in verified claims instead of request parameters. Input is validated against the contract, queries are parameterised, secrets come from a secrets manager with rotation, and service-to-service traffic uses mutual TLS. Audit logs record who did what to which resource and are written append-only.

How it scales

Services are stateless and scale horizontally behind a load balancer, with state in the database, cache and broker. The database is usually the limit, so scaling proceeds in order: indexes and query plans, connection pooling, caching, read replicas, table partitioning, and sharding only when a single primary can no longer absorb writes. Queues absorb bursts, and bounded concurrency with load shedding keeps an overloaded service responsive for the requests it does accept.

How it is monitored

OpenTelemetry provides distributed traces across HTTP, gRPC and message boundaries, with trace context carried in message headers so asynchronous hops remain visible. Metrics cover rate, errors, duration and saturation per endpoint, along with consumer lag, pool utilisation and cache hit ratio. Logs are structured, carry correlation identifiers and exclude personal data and secrets.

How it is deployed

Services ship as container images through a pipeline that runs tests, static analysis and dependency and image scanning, then deploys progressively with rolling or canary releases and automated rollback on error-rate or latency regressions. Database migrations are backward compatible and applied separately from code, following expand, migrate, contract. Feature flags control exposure of new behaviour independently of deployment.

How it integrates

External systems are reached through adapters that isolate their data models and failure behaviour from the core domain. Inbound webhooks are verified by signature, persisted, acknowledged quickly and processed asynchronously with deduplication; outbound webhooks are signed and retried with backoff. Legacy systems are integrated through change data capture or an anti-corruption layer instead of shared database access.

Technologies

Chosen to fit the workload and your existing estate.

  • Java and Spring Boot
  • Go
  • Node.js
  • Python and FastAPI
  • PostgreSQL
  • MySQL
  • MongoDB
  • Redis
  • Apache Kafka
  • RabbitMQ
  • OpenSearch
  • gRPC
  • GraphQL
  • OpenTelemetry

From zero to production

The engineering lifecycle

  1. 01

    Idea

  2. 02

    Discovery

  3. 03

    System architecture

  4. 04

    UX / product design

  5. 05

    Application engineering

  6. 06

    AI / data layer

  7. 07

    Cloud infrastructure

  8. 08

    Security

  9. 09

    QA & performance

  10. 10

    Deployment

  11. 11

    Observability

  12. 12

    Continuous evolution

FAQ

Common questions

What is idempotency and why does it matter?

An idempotent operation produces the same result whether it is executed once or several times. It matters because in a distributed system a timeout does not reveal whether the request was processed, so the only safe response is to retry. Idempotency keys on APIs and deduplication in message consumers make that retry harmless.

Does Kafka give exactly-once delivery?

Kafka offers idempotent producers and transactions that give exactly-once semantics for read-process-write cycles that stay within Kafka. As soon as a consumer writes to an external database or calls another API, the guarantee is at-least-once, and the consumer must be idempotent, typically by recording processed message identifiers in the same transaction as its state change.

How should an API be versioned?

Prefer evolution over versions: add optional fields and new endpoints, never change the meaning of an existing field, and have clients ignore unknown fields. When a breaking change is unavoidable, publish a new major version in the path or a header, run both in parallel, announce a deprecation timeline and measure remaining usage of the old version before removing it.

When is a distributed cache worth adding?

When profiling shows that read load or an expensive computation is the bottleneck and the data can tolerate a defined staleness. A cache adds an invalidation problem and a new failure mode, so it should follow query and index optimisation, not replace it. Each cached item needs an owner, a time to live and an invalidation trigger.

Can a backend be moved from a monolith to services gradually?

Yes, with the strangler pattern. A routing layer in front of the monolith sends selected paths to new services, one bounded context at a time, while change data capture or events keep data in step during the transition. The first step is usually to enforce module boundaries inside the monolith, since a module that cannot be isolated in-process will not be isolated by a network either.

Have a system that needs to work in production?

Tell us what you are building. An engineer will read it and reply within two working days.