Skip to content
Starone

Capability

Software architecture that holds up under change and load

We design systems around domain boundaries, failure modes and the team that will run them, then build them with the practices that keep them changeable.

Overview

Software engineering is the practice of building systems that remain correct and changeable over years, under load, while different people work on them. Architecture is the part that is expensive to change later: where the boundaries lie, which component owns which data, how components communicate and what happens when one of them fails.

Most architecture problems are boundary problems. Services that share a database cannot be deployed independently. Modules that reach into each other's tables cannot be reasoned about separately. A synchronous chain of five calls is only as available as the product of all five. Getting boundaries right matters more than the choice between a monolith and microservices, and the same domain analysis serves both.

Starone's architects choose structure from constraints: team size, rate of change, consistency requirements, load profile and operational maturity. This page covers architecture and engineering practice. Frontend, backend and mobile engineering each have their own capability page.

What we engineer

Technical areas

01

Architectural styles

A modular monolith is one deployable unit with enforced internal boundaries. Microservices are independently deployable services that each own their data. Service-oriented architecture sits between them, with coarser services that often share infrastructure. We select the style that matches how many teams need to release independently and how different the scaling needs of each part are.

  • Modular monolith
  • Microservices
  • Service-oriented architecture
  • Hexagonal architecture
  • API gateway and backend-for-frontend
  • Strangler-fig migration
02

Domain-driven design

Domain-driven design finds boundaries by studying the business, not the database schema. A bounded context is an area where a term has one meaning and one owning team, and an aggregate defines what must change together in a single transaction. These boundaries become modules or services and are the most durable decision in the system.

  • Bounded contexts and context mapping
  • Aggregates and invariants
  • Ubiquitous language
  • Domain events
  • Anti-corruption layers
03

Event-driven and asynchronous systems

Events decouple a producer from the consumers that react to it, in time and in availability. Work that does not need an immediate answer moves to queues, which absorbs bursts and isolates slow dependencies. The cost is eventual consistency and harder debugging, so we use events where decoupling is worth it and direct calls where the caller needs the result.

  • Kafka and RabbitMQ messaging
  • Transactional outbox
  • Idempotent consumers
  • Sagas for multi-step transactions
  • Dead-letter queues and replay
  • Schema-versioned events
04

Distributed systems and data patterns

Once a system spans processes, networks fail, messages arrive twice and clocks disagree. We design for this explicitly: choosing consistency per operation, making writes idempotent, and separating read and write models with CQRS where their shapes or loads differ sharply. Event sourcing is used when the history of changes is itself a requirement, as in ledgers and audit-heavy domains.

  • CQRS
  • Event sourcing
  • Idempotency keys
  • Optimistic concurrency control
  • Distributed caching
  • Consistency and availability trade-offs
05

Fault tolerance and horizontal scaling

Fault tolerance means a failure in one dependency degrades one feature, not the whole system. Horizontal scaling requires stateless services, with state held in data stores built for it. Both are designed in from the start through timeouts, bounded retries and load shedding, and verified with load and failure testing.

  • Timeouts and retries with backoff
  • Circuit breakers and bulkheads
  • Rate limiting and backpressure
  • Stateless services and autoscaling
  • Partitioning and read replicas
  • Graceful degradation
06

Engineering practice

Architecture survives only if daily practice protects it. We use a layered testing strategy, review every change, integrate continuously and treat API contracts as versioned artefacts. Significant decisions are recorded as architecture decision records, so the reasoning is available to whoever maintains the system next.

  • Unit, integration and contract tests
  • Code review and trunk-based development
  • Continuous integration
  • OpenAPI and gRPC contracts
  • Architecture decision records
  • Tracing, metrics and structured logs

Modular monolith or microservices

Microservices solve an organisational problem: many teams releasing on independent schedules, with parts of the system that scale or fail differently. They pay for that with network calls in place of function calls, distributed transactions, a pipeline per service and a much larger operational surface.

A modular monolith gives most of the structural benefit at lower cost. Modules have explicit public interfaces, own their tables and communicate through in-process calls or internal events, with boundaries enforced by build rules or architecture tests. There is one deployment, one transaction boundary and one place to debug.

The practical path is to begin modular and extract a service when there is a specific reason: a module needs its own scaling profile, release cadence, technology or isolation. If boundaries were kept clean, extraction is mechanical. If they were not, microservices would have produced a distributed monolith, which combines the drawbacks of both.

Architecture

Microservices architecture
  1. Clients

    • Web app
    • Mobile apps
    • Partner integrations
  2. API Gateway / BFF

    • Routing
    • Authentication
    • Rate limiting
    • Response shaping
  3. Domain Services

    Each service owns one bounded context

    • Orders
    • Payments
    • Catalogue
    • Accounts
    • Notifications
  4. Async Messaging

    • Kafka topics
    • Outbox publisher
    • Event consumers
    • Dead-letter queues
  5. Data Stores

    Database per service, no shared tables

    • PostgreSQL per service
    • Redis cache
    • Search index
    • Object storage
  6. Platform

    • Tracing and metrics
    • Central logging
    • CI/CD pipelines
    • Service configuration
Independently deployable services behind a gateway, each owning its data and communicating through events where a synchronous call is not required. The platform layer is what makes this many moving parts operable.

Consistency across service boundaries

A single database gives atomic transactions. Across services there is no such guarantee, and two-phase commit couples availability in ways most systems cannot accept. The standard answer is a saga: a sequence of local transactions, each publishing an event, with compensating actions when a later step fails.

Two mechanisms make this safe. The transactional outbox writes the state change and the outgoing event in the same database transaction, so an event is never lost and never published for a change that rolled back. Idempotent consumers make redelivery harmless, since brokers deliver at least once in practice. With both in place, eventual consistency is predictable.

Not every operation tolerates it. Balances, inventory reservations and uniqueness checks usually need strong consistency, so we keep those invariants inside one aggregate and one service.

Designing for partial failure

In a distributed system, partial failure is the normal state. A dependency that responds slowly is worse than one that is down, because callers hold threads and connections while they wait and the slowness spreads upstream.

Every remote call therefore gets a timeout derived from the caller's own latency budget, retries that are bounded and jittered, and a circuit breaker that stops calls to an unhealthy dependency. Bulkheads give each dependency its own connection pool so that one cannot exhaust shared resources. When the system is overloaded it sheds low-priority work early instead of queuing without limit.

  • Timeouts set from an end-to-end latency budget
  • Retries with exponential backoff and jitter, on idempotent calls only
  • Circuit breakers and bulkheads per dependency
  • Bounded queues with backpressure and load shedding
  • Fallback behaviour defined for each critical feature

How we engineer it

Our approach

Architecture work starts with the domain and the constraints, and is validated in running code early.

  1. 01

    Understand the domain

    Work through the business processes with the people who run them, and map bounded contexts, key entities and the events that matter.

  2. 02

    State the quality attributes

    Write down availability, latency, throughput, consistency and compliance constraints as measurable targets. These drive structure more than features do.

  3. 03

    Choose and record the architecture

    Select the style, data ownership and communication patterns, and capture each significant choice as an ADR with the alternatives that were rejected.

  4. 04

    Build a walking skeleton

    Deliver one thin slice through every layer, including CI, deployment and observability, before widening scope. It tests the architecture against reality while change is cheap.

  5. 05

    Deliver in increments with guardrails

    Ship small changes behind automated tests, contract checks and code review, with architecture tests that fail the build when a boundary is crossed.

  6. 06

    Verify under load and failure

    Run load tests and inject dependency failures before launch, then revisit decisions as real traffic data arrives.

Trade-offs

Decisions that shape the design

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

Should we start with microservices?
Usually not. Start with a modular monolith unless several teams already need independent release cycles, or parts of the system have clearly different scaling or isolation needs. Extract services when a specific boundary demonstrates that need.
Synchronous APIs or asynchronous events between components?
Use synchronous calls when the caller needs an answer to continue, such as a query or a validation. Use events when other parts merely need to know that something happened, when work can be deferred, or when the producer must stay available while a consumer is down. Avoid long synchronous chains, since each hop lowers availability and adds latency.
Is CQRS worth introducing?
Introduce it when read and write workloads differ enough that one model serves neither well: complex queries over data written through strict invariants, or reads that outnumber writes by orders of magnitude. For ordinary CRUD it adds two models to keep in step for little return. Event sourcing is a separate decision and should be reserved for domains that need a full history.
REST, gRPC or GraphQL?
REST with OpenAPI suits public and partner APIs because it is widely understood and cacheable. gRPC suits internal service-to-service calls that benefit from strict contracts, streaming and low overhead. GraphQL suits client-facing aggregation where many screens need different shapes of the same data, and it needs query cost controls.
One database or a database per service?
Each service or module should own its data and expose it only through its interface. In a modular monolith this can be separate schemas in one PostgreSQL instance, which keeps operations simple while preserving ownership. Physically separate databases are warranted when services need independent scaling, different storage engines or failure isolation.
Rewrite the legacy system or migrate it incrementally?
Migrate incrementally in almost every case. A strangler-fig approach routes one capability at a time to new code behind a facade, keeps the business running and yields feedback early. A full rewrite is justified only when the existing system cannot be changed safely at all, and even then it should be delivered in slices.

Production readiness

What makes it production-ready

How it is secured

Security is part of design: threat modelling at the architecture stage, authentication and authorisation enforced at every service boundary, input validation, secrets held in a vault and dependencies scanned in CI. Services run with the least privilege they need.

How it scales

Services are stateless and scale horizontally behind load balancers, with state in databases, caches and brokers that are partitioned or replicated as needed. Capacity limits are found by load testing, not by incident.

How it is monitored

Every service emits structured logs, metrics and distributed traces with a shared correlation identifier. Service-level objectives define what healthy means, and alerts are tied to symptoms a user would notice.

How it is deployed

Each change passes automated tests and builds an immutable artefact that is promoted unchanged through environments. Releases use rolling, blue-green or canary strategies, database migrations are backwards compatible, and feature flags separate deployment from release.

How it integrates

Integration happens through versioned contracts: OpenAPI or gRPC for calls, schema-registered events for messaging. Anti-corruption layers isolate the domain model from third-party and legacy systems, and consumer-driven contract tests catch breaking changes before release.

Technologies

Chosen to fit the workload and your existing estate.

  • Java
  • Spring Boot
  • Go
  • TypeScript
  • Node.js
  • PostgreSQL
  • Redis
  • Apache Kafka
  • RabbitMQ
  • gRPC
  • OpenAPI
  • Docker
  • Kubernetes
  • 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

How do you decide on an architecture for a new system?

From constraints. We look at the domain boundaries, the number of teams, required availability and consistency, expected load and the organisation's ability to operate what is built, then pick the simplest structure that satisfies them and record why.

Can you work with our existing codebase?

Yes. We begin by reading the code and its operational history, add tests around the areas that will change, and improve structure incrementally alongside feature work.

How much testing is enough?

Enough that the team can change the system with confidence. In practice that means fast unit tests for domain logic, integration tests against real dependencies such as the database, contract tests at service boundaries, and a small number of end-to-end tests for critical journeys.

What documentation do you leave behind?

Architecture decision records, context and container diagrams, API specifications, runbooks for operational tasks and a README that gets a new engineer to a running system. Documentation lives in the repository so that it changes with the code.

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.