Skip to content
willowark

Distributed systems that fail gracefully instead of loudly

Distributed systems work covers software that runs across multiple services, machines, or sites — event streams feeding several consumers, plants reporting to a central platform, workloads too large or too critical for one box. Engineered well, it removes the twin risks of scale: the single server that caps your growth, and the outage that takes everything down at once.

Willowark's first question is whether you need distribution at all, because every network hop you add is a new way to fail. When the answer is yes, we make the hard tradeoffs explicitly: which data must be strongly consistent, where eventual consistency is fine, and what each component does when its neighbor is unreachable. Systems fail by design or by surprise; we prefer design.

Illustrative: an engineer's desk with code and a system diagram on two monitorsSoftware Engineering

How the work gets done

The same way every time: scope, build, hand over.

The building blocks are message brokers like Kafka, RabbitMQ, or cloud queues, with delivery semantics chosen deliberately — at-least-once with idempotent consumers is usually the honest choice over exactly-once promises. Multi-step operations spanning services use the outbox pattern and sagas with compensation, so a failure midway leaves the system correctable instead of corrupted. Backpressure is designed in, so a slow consumer degrades throughput rather than toppling the pipeline.

Observability cannot be bolted on afterward, so every message carries correlation IDs and every service emits traces, so you can follow one misbehaving request across six services in a single view. We validate failure behavior by inducing it: kill a node, partition the network, replay a backlog, and confirm the system does what the design says.

Scoping starts by drawing the system as it would fail. For each component we ask what it does when its upstream is silent, when its downstream is slow, and when it receives the same message twice — and we write those answers down before choosing any technology. The trade-offs are unavoidable and we make them explicitly: stronger consistency costs availability during a partition, lower latency costs buffering that protects against bursts, more services cost more operational surface. Where a requirement is soft, we choose the simpler side. Where it is hard — money, safety, regulatory records — we pay for the stronger guarantee and prove it under test.

Distributed systems rarely fail because of a bug in one service; they fail because of an assumption between two. Clock drift between sites, a message schema that changed on one side, a retry storm when a dependency came back up — these are the incidents we design against with versioned message contracts, bounded retries with jitter, and consumers that tolerate reordering and duplicates. Handover is heavier than for a single application: a topology diagram with the failure behavior annotated, dashboards per service, and runbooks for the scenarios we rehearsed. We consider the work done when your team has run a failure drill themselves and the system behaved as documented.

  1. Scope it in writing

    What we agree before work starts

    • Architecture design with explicit consistency and failure-mode decisions
    • Event streaming or queue infrastructure with chosen delivery semantics
  2. Build with checkpoints

    Working results, not slide decks

    • Idempotent consumers with saga or outbox patterns for multi-service operations
    • Distributed tracing with correlation across every service
  3. Hand over something you own

    Documentation, source, and training

    • Failure-injection test results validating behavior under partition and load
    • Operational runbooks and dashboards per service, with rehearsed failure-drill procedures

Sound familiar?

Where distributed systems earns its keep.

Sensor data from a dozen sites that must keep collecting locally when the WAN link drops

An event pipeline where one slow consumer currently stalls every downstream system

One stream of events fanned out to billing, analytics, and notifications without drift

A central platform that must keep accepting orders while one regional site is down for maintenance

Common questions

Asked before every distributed systems project.

Do we actually need a distributed system?

Maybe not, and we will tell you if so. A single well-built service on modern hardware goes remarkably far, and distribution adds operational cost that never goes away. The genuine triggers are physical distribution across sites or edge devices, availability a single node cannot meet, or load that provably exceeds vertical scaling.

What consistency guarantees will we get?

The ones we choose together, stated plainly: which reads can be stale and by how much, which operations are atomic, what happens to in-flight work during a partition. Most business systems want eventual consistency almost everywhere and strong consistency in a few money-touching places. Incidents come from assuming consistency nobody engineered.

Can this span our plant floors and the cloud?

Yes — that is a pattern we specifically design for, with store-and-forward at each site so local operations continue through WAN outages and data reconciles when the link returns. Plant-floor networks, protocol conversion, and cloud ingestion each have their own constraints, and the architecture respects all three.

What message broker should we use?

The one that matches your delivery needs and your team's ability to run it. Kafka suits high-volume streams that several consumers replay independently, but it is real operational work to host; RabbitMQ and cloud-native queues cover most business workloads with far less care. We usually lean toward a managed service from your cloud provider unless throughput, retention, or on-premises constraints rule it out. Choosing the heaviest tool is a common and expensive mistake.

How do you test something that only fails under real conditions?

By manufacturing the conditions. Staging gets a load generator that replays realistic traffic shapes, including the bursts and backlogs that expose weak points, and we inject failures deliberately: stop a broker node, cut the link to one site, slow a database, feed duplicate messages. Each drill has a written expectation and a recorded result. It does not catch everything — production always has one more surprise — but it catches the failures that would otherwise be discovered by customers.

Where this sits

Distributed Systems, inside a software engineering system.

The lit component is the part of the system this service delivers; the rest is what it has to work with.

A production web applicationRESTRESTSQLeventswebhookUsersWeb appAPIDatabaseWorkersjobs, queuesIntegrationsexisting tools

Hover or focus a component to see what it is and what it talks to. Arrow keys move between them.

Users reach a web app that talks to an API over a database, with background workers for the slow work and integrations to the systems already in use.

Components:

  1. Users: Staff, customers, or both.
  2. Web app: The interface: fast, accessible, works on a phone.
  3. API: Typed, versioned, authenticated.
  4. Database: Postgres, usually. Backed up, migrated, monitored.
  5. Workers (jobs, queues): Reports, syncs, emails — anything that should not block a click.
  6. Integrations (existing tools): Accounting, CRM, email, payments.

Connections:

  • Users to Web app over REST
  • Web app to API over REST
  • API to Database over SQL
  • API to Workers over events
  • Workers to Integrations over webhook, both directions
A typical architecture, drawn to explain the pattern — not a specific client's system.

Strategy. Software. Systems.

Have a system that should exist?

Tell us what your operation is doing manually, what isn't connected, or what you're trying to build. We'll tell you plainly whether and how we can help.