Platform engineering

This is not a diagram of what I would build.
It is the fleet that is live right now.

I designed, built and operate single-handedly a multi-tenant SaaS ecosystem in production, with real paying customers. This page shows the measured numbers, how a release happens, and — the part that matters — the incidents that took the system down and what changed after each one.

Every number below was measured on the fleet, not estimated
A 30-second read

What I design, build, operate and measure

This table exists so you do not have to read the whole page. The column that matters most is the last one: measuring is what separates operating from assuming. And where I only have training and not practice, it says so — ticking every box informs nobody.

Domain DesignBuild OperateMeasure
Linux, networking and diagnosis firewall with SSH only · network segmented into two subnets · lock contention diagnosis in the database
Containers and composition 224 containers · declared CPU and concurrency limits · one image per repository
Continuous delivery and release strategy canary with validation and rollback by digest · fleet lock · release ledger
Observability and incident response 9 targets · 12 rules · three written post-mortems, each with the guard it left behind
Databases and continuity primary + hot replica · continuous WAL · RTO and RPO timed
Security, identity and multi-tenancy zero exposed port · fail-closed isolation in the database · in-house OAuth2 and JWKS · secrets vault
Distributed architecture and messaging transactional outbox · 12 consumer groups · active deduplication · watched failure queue
Infrastructure as code authored Terraform modules for managed cloud (network, database, security, observability) — not what I operate today: the fleet is declarative through composition
Kubernetes training completed and manifests written; a deliberate decision not to operate it — the reasoning is in Act III
Third-party managed cloud training completed; the real operation runs on public cloud with in-house services — I say this on purpose
in production, today partial or outside production no — and I say so
I

What is at stake

This is not a lab. There are paying customers, and when it goes down somebody stops selling. Everything else on this page exists because of that sentence.

The fleet

The size of the operation

31
microservices with a production deployment
224
containers live (112 per node)
87
releases per day (13-day average)
11.047
automated tests across 3 of the 31 services

Two active-active nodes

ARM64 on public cloud, 8 vCPU and 30 GB each, behind a Cloudflare tunnel. Both serve traffic: there is no idle node waiting for a disaster.

Multi-tenant with isolation in the database

Row-Level Security in PostgreSQL, fail-closed: without the customer scope in the session, the query returns zero rows. The runtime role does not have BYPASSRLS.

A quality gate that actually fails

A minimum coverage that fails the build: 95% on two services, 90.3% on another. It is not a poster target — it is a condition for releasing.

Where each number above comes from
the question I would ask if I were you — and the answer, with the limit of each measurement
open

A number without provenance is an opinion wearing the costume of data. Each line below says what was counted, how, when — and what the measurement does NOT prove. The last column is the one that interests me: an honest measurement states its own limit.

NumberHow it was measuredWhat this does NOT prove
31 count of production composition files across the repositories that all are active at the same time — it counts what has a deployment defined
224 docker ps on both nodes: 112 each. Of those 112, 59 are application processes (web, worker, publisher, consumer, scheduler); the other 53 are bus, data, observability, edge and vault it is not a measure of load or utilisation — it is a count of processes
87 release ledger of one node, 1,212 lines over 14 days. Each line = one service recreation with date, digest and commit. Median 79; ranges from 2 to 172 per day it is not “87 new versions”: one logical release recreates the service on both nodes, and the average hides the variation
11,047 tests actually executed in a real run of 3 services. In parallel: 31,438 test functions defined across 14 services (static count) the two numbers measure different things, and neither one is coverage — coverage is the floor that fails the build
RPO measured just now: replication lag of 1.01 s and zero bytes of WAL pending delivery to the standby it is a sample, not a test of losing the primary. The real RPO under failure is only known by causing the failure — that is on the plan, not in the record
47 min an exercise of provisioning a node from zero and bringing the fleet up on it, in September 2026, the day after the migration to the current cloud (instances created on the 12th and 16th, checked in the provider's metadata). Definition of “ready”: authenticating with username and password and getting into the system — which exercises edge, tunnel, routing, identity, database and session end to end it does not prove the 31 services were healthy nor that background work was already processing — it proves the user's critical path was up. It is an end-to-end criterion, stronger than “the container started” and narrower than “the whole fleet converged”
12 min time for the entire fleet to come up and converge, timed in the same September 2026 exercise, with the same end-to-end criterion same caveat: one exercise, on one date, with one provider. Repeating it on a different provider is what would turn the number into evidence of portability, not only of recovery
12 rules alert file read straight from the metrics service; 9 active targets, all responding at the moment of reading a rule existing is not a tested alert: not every one has been fired on purpose to check the path all the way to me

“Show me the diagram and I am sceptical. Show me what happens when it fails and I believe you.”

what this page tries to answer
II

How it holds up

The full picture: where traffic comes in, where each piece runs, how the data is reached — and what happens when each part fails.

Before the infrastructure: the system seen from outside, and from within

Two views that come before the deployment drawing. The first answers “who uses this and what outside world does it talk to”. The second answers “which applications exist and how do they talk to each other” — with no infrastructure at all, on purpose: mixing the two in a single diagram is the mistake that makes most of them unreadable.

Context diagram: who uses the system and what external world it talks to LEVEL 1 · CONTEXT who uses it, and what outside world it talks to Customer's operator sells, buys, invoices End customer portal, receipt, signature Platform operator one person — me Zenithus multi-tenant SaaS 31 microservices a single origin for every screen Acquirer · POS card and reconciliation Bureaus · Open banking credit and statements Government APIs tax and labour E-signature · Marketplace contracts and external sales Messaging WhatsApp and e-mail everything going out passes through ONE integration service Trust boundary no actor reaches a service directly: every entry goes through the managed edge and the in-house identity ● person ■ the system ▣ external system
Container diagram: which applications exist and how they talk to each other LEVEL 2 · CONTAINERS the applications and how they talk — no infrastructure, on purpose User's browser ONE ORIGIN — everything below answers under the same address Identity Java · Spring Boot issues the token publishes the keys standard OAuth2/OIDC endpoints Application shell Python · Django menu, customer context and session — it embeds the verticals 12 business verticals Python · Java · Go sales · inventory · tax finance · HR · fleet… mounted by path Control Plane who the customer is, which plan they have, whose child they are SYNCHRONOUS call, typed Event bus one fact → 12 groups dedup · failure queue DEFAULT path: asynchronous Integration 22 connectors · the ONLY one that talks to third parties returns a NORMALIZED fact PostgreSQL — customer isolation inside the database itself one database per vertical · the who-sees-what rule does not depend on application code The infrastructure (nodes, network, balancing) is in the deployment diagram just below — here, applications only.
Zenithus fleet architecture — logical and physical diagram From user to database: Cloudflare edge with WAF and outbound tunnel, no inbound port on the firewall, in-house identity serving OAuth2 and JWKS on the same single origin where the verticals are mounted by path, network segmented into an application subnet and a data subnet, two active-active nodes, primary PostgreSQL with a hot replica and continuous WAL archiving, backup off the provider, and a cross-cutting layer of authorization, observability and continuous delivery. Single points of failure are marked. Zenithus architecture — logical and physical Measured on the fleet, not estimated · identifiers and addresses omitted on purpose 12 minRTO — entire fleet 47 minRTO — new node from zero secondsRPO — continuous WAL 1 Users, partners and APIs HTTPS MANAGED EDGE — CLOUDFLARE the attack dies before reaching the node DNS + TLS managed certificate WAF filter before the origin 2 Tunnel — one connector per node OUTBOUND connection; draining = releasing risk-free R2 — backup off the provider dump every 3h · weekly base · media · retention No egress billed · no lock-in the origin can change provider without a rewrite SYSTEM FIREWALL — allowed services: SSH only. Ports 80 and 443 do not exist in the rule: traffic comes in because the node goes OUT. OCI · São Paulo region · private virtual network Accepted SPOF: both nodes in the same availability domain APPLICATION SUBNET · /24 no public IP · egress through NAT NODE A — canary (releases first) ACTIVE ARM64 · 8 vCPU · 30 GB · 112 containers cloudflared Traefik — routes host + path 3 IN-HOUSE IDENTITY (Spring Boot) — OAuth2 + JWKS authentication and keys on the SAME origin as the screens session on one domain only: no CORS, no third-party cookie SINGLE ORIGIN — 12 verticals mounted by PATH /app/<vertical> — one prefix per business domain a shell hosts the rest; no vertical has a public route of its own Local bus — 14 Redis (one per vertical) transactional outbox → relay → Streams → inbox with dedup → DLQ 5 HAProxy — single database door (one backend; manual promotion) NODE B — stable ACTIVE identical · neither one is an idle standby cloudflared Traefik 4 SINGLE PLATFORM SDK one authorization implementation for every vertical — not 12 diverging copies scope per request, fail-closed: without scope, the call is DENIED machine-to-machine: one key per service plan and tenant truth comes from the Control Plane POLYGLOT STACKS, ONE CONTRACT Java/Spring Boot · Go · Rust · Python the contract is the event and the scope, not the language Local bus — 15 Redis · queue on disk PostgreSQL 16 HOT REPLICA streaming active separate instance SSH closed + pg_receivewal archives WAL continuously point-in-time recovery read, not write DATA SUBNET · /24 — segmented from the application reachable only from inside the private network pgbouncer entering the path transaction pooling — dozens of services without opening connections at will PostgreSQL 16 — PRIMARY dedicated host, outside the app nodes · 24 databases customer isolation by Row-Level Security, fail-closed runtime role without BYPASSRLS: forgetting the scope returns zero rows KNOWN SPOF the primary is the single point. No automatic failover — on purpose: avoids split-brain. continuous replication → ⑥ encrypted backup off the provider CONTROLLED EGRESS — ONE DOOR TO THE WORLD measured: zero third-party SDKs in the business verticals 7 Integration — 22 connectors acquirer · bureau · government · open banking e-signature · marketplace · messaging · storage Circuit breaker · retry with backoff · idempotency a slow third party does not become a stuck queue in here credentials come from the VAULT, never from the environment Third-party callback: SEPARATE origin for webhooks HMAC signature verified — unsigned payload is discarded the third party never touches the screens' origin the result returns to the ecosystem as a NORMALIZED fact on the bus — the consumer never talks to the third party CROSS-CUTTING LAYER — applies to everything above SECRETS OpenBao vault · one token per consumer least privilege · MFA on critical operations dedicated alert for an expiring token OBSERVABILITY Prometheus · Grafana · Alertmanager cAdvisor · exporters · external probes 9 targets · 12 rules — 3 watch for ABSENCE CONTINUOUS DELIVERY branch → PR → local gate → canary validates on the drained node · rollback by digest production never compiles · global fleet lock CONTINUITY dump every 3h · weekly base · continuous WAL destination off the provider · verified restore ledger: date, digest and commit of every service WHAT HAPPENS WHEN IT FAILS — the part a pretty diagram usually omits An application node goes down The other already serves 100% of traffic. The stopped node's queue is on disk and resumes when it returns. User impact: none. No human action. The primary database goes down A real outage. The hot replica exists and is current; promotion is manual, with a written runbook. Deliberate: automating without fencing gives split-brain. The whole region is lost The encrypted backup lives off the provider. A new node provisioned and the fleet up: 47 minutes, measured. Data loss: seconds, thanks to continuous WAL.
managed edge compute and private network data and continuity security boundary · accepted single point of failure
  Why the single points of failure are marked. A diagram that hides the SPOF is not a diagram, it is a brochure. The three that exist here — both nodes in the same availability domain, the primary database without automatic promotion, and the single database door — are choices, and each one has its reason written down further below.
The platform

Layer by layer

The 224 containers are not 224 copies of the same thing. Counted by role, they draw the platform — and every layer exists because some pain asked for it.

Edge — no port open to the internet

Neither node exposes a public port. Traffic comes in through a Cloudflare tunnel (outbound connector) and lands on Traefik, which does host-based routing, automatic TLS and acts as the balancing point. Draining a node from the tunnel is what makes canary releases possible: that is how you pull a node out of the balancer without taking anything down.

Data — pooling, proxy and isolation in the database

PostgreSQL on a dedicated host, shared by the verticals, with HAProxy as the single access point and pgbouncer provisioned but still outside the path (measured: zero active clients; the applications go straight to HAProxy) — it enters when the connection count calls for it. There are two instances: primary and hot replica. Customer isolation is Row-Level Security, not WHERE tenant_id scattered through the code: the runtime role has no BYPASSRLS, so forgetting the scope returns zero rows instead of leaking.

Honesty about the target: there is no automatic database failover. Promotion is manual and documented — and I would rather say that than draw a Patroni that does not exist.

Messaging — 12 relays, 12 consumers, 10 schedulers

Services do not call each other in a chain: they publish facts. The path is a transactional outbox (the fact commits together with the data, or neither does) → relay → Redis Streams → inbox with dedup per consumer group → DLQ for whatever failed. There are 14 Redis instances: the bus is per vertical and per node, and that is exactly why convergence is settled in Postgres, not in a distributed lock that would not cross the nodes.

Secrets — a vault, not a .env file

Credentials live in a vault (OpenBao) with a per-consumer service token. There is a dedicated alert for a vault token nearing expiry — because a secret that expires silently brings everything down at once, at the worst hour.

How the services talk — and why it is almost never a direct call

The rule is to publish a fact, not to call a service. The default path is asynchronous: transactional outbox (the fact only exists if the data exists), relay, queue, inbox with deduplication per consumer group and a DLQ for whatever failed. That means a service that is down delays the other one — it does not take it down with it.

When the answer is needed now — “is this customer entitled to this feature?”, “is this company a child of that one?” — the call is synchronous, but it is never loose HTTP scattered through the code: it goes through a typed client from the single SDK, with named operations and machine-to-machine authentication by a key of its own for each service. Authority over plan, hierarchy and entitlement has a single owner; the others ask, they do not infer.

The same SDK is loaded in all 9 Python verticals — one authorization implementation, not nine diverging ones

The event bus — one fact, twelve readers, no coupling

When a sale happens, the sales service notifies nobody. It publishes the fact — and twelve independent consumer groups read it at their own pace. Inventory draws down the item, finance creates the receivable, tax prepares the document, accounting posts the entry. None of them knows the others exist, and one of them being down delays its own queue, not the sale.

the write and the fact in the SAME transaction relay publishes one stream per domain 12 consumer groups inbox discards repeats DLQ for what failed
10.003facts on the platform stream
12independent consumer groups
242registered consumers
0pending — the bus is up to date
5.362active deduplication markers
20in the failure queue, under watch

  Workers with a written limit

Per node: 13 workers, 11 publishers, 10 consumers and 10 schedulers. Each runs with concurrency 1, declared in the composition file. The tool's default is 8 — and that is how the fleet went down once. A limit that is not written does not exist.

  Queues separated by nature

AI work, analytics work and routine work have queues of their own. A model call that takes thirty seconds cannot sit in front of the queue that confirms a sale. Separating is what stops the slow from delaying the urgent.

  Queue on disk: delay, not loss

The bus writes to disk. If a node goes down with queued work, that work does not evaporate — it waits for the node to come back and resumes. The difference between “the customer waited” and “the order vanished” is this configuration line.

  The distributed detail that cost me the most to understand. The bus is per node — each one has its own. That means a distributed lock through the bus does not work between the two: each node would see its own lock and both would run. Since the database is the only truly shared resource, convergence is settled there, with an atomic claim on the row — whoever takes it first runs, the other moves on without blocking. It is not leader election, and that is deliberate: election concentrates the work on one node and reduces the parallelism the two nodes exist to provide.

Observability — and the care to alert on absence

Prometheus scraping 9 targets (both nodes, cAdvisor, the PostgreSQL exporter, Traefik, HTTP probes), Grafana, Alertmanager, cAdvisor, node and postgres exporter, blackbox probing routes from the outside in. 12 alerting rules live.

What I consider the mature part is not the quantity — it is what they watch. Three of them do not ask “did something break?”, they ask “did something stop happening?”: off-site backup empty, backup late, backup missing the database inside. A backup vault that exists but is empty passes as healthy in any presence check — and you only find out on restore day. The others cover PostgreSQL going down, disk filling up on each node, a node off the air, a WAL slot accumulating and a container in a restart loop.

9 of 9 targets responding at the time of this measurement

“The best tool is planning.”

why each piece is where it is
III

Why it is this way

Every choice here has a gain, a cost I pay and a commercial consequence. Anyone listing only the gain is selling, not designing.

Docker Compose — and not Kubernetes or k3s
the decision I am asked about most, and the one with the clearest trade-off
see trade-off

The entire compute budget is two nodes of 8 vCPU and 30 GB, and they already run 224 containers. A Kubernetes control plane — API server, etcd, scheduler, controllers, overlay network, ingress controller — consumes CPU and memory from that same budget, continuously. And sustained CPU load is exactly what has already cost me two infrastructure suspensions.

what I gain A mental model one person alone can operate at 3 in the morning. The deployment unit is the same thing that runs on my machine. No overlay network, no ingress controller, no cluster upgrade cycle, no custom resources to debug during an incident.
what I pay for it I have no automatic rescheduling across nodes and no horizontal autoscaling. Capacity is a human decision. If both nodes saturate at once, nothing grows on its own — I would have to provision, and that takes minutes, not seconds.

As a business decision: Kubernetes solves a scale problem I do not have yet, charging today in operational complexity and in consumption of the scarcest resource I have. While the load fits in two nodes, adopting it would mean paying up front for insurance against a risk that does not exist — and creating a new, real one: a surface I cannot sustain alone while on call. This is not rejecting the tool; it is not switching tools out of fashion. The day the maths changes — more than one operator, or load that does not fit in two nodes — the migration is already designed, and it is cheap precisely because everything is already container images and declarative configuration.

Outbound tunnel — and not a balancer with a public IP
the attack surface that ceases to exist
see trade-off
what I gain There is no inbound door to scan. The firewall allows only SSH; 80 and 443 are not in the rule. Application filtering, certificates and volumetric mitigation sit on the managed edge, before a single byte touches my node. And draining a node from the balancer is stopping one process — that is what makes canary releases possible.
what I pay for it A dependency on a third party in the critical inbound path: if the edge goes down, the system is unreachable even while running. It is one more vendor in the chain, and I do not control their maintenance window.

As a business decision: a managed balancer costs per hour and per volume, and still hands me back a public IP to defend. The tunnel costs nothing on the plan in use, removes the exposed surface and delivers application filtering along with it. I traded a risk I would have to operate (exposure) for a risk I monitor (third-party availability) — and that second one has an external probe and an alert.

Manual database promotion — and not automatic failover
the most uncomfortable decision to defend, and the most correct
see trade-off
what I gain Split-brain becomes impossible. Two instances accepting writes at the same time, in a multi-tenant system that moves money, is damage no backup properly undoes — because both sides end up “right”.
what I pay for it The primary database is a declared single point of failure. If it goes down there is a real outage until promotion, and the time depends on a human answering. I do not hide that in the diagram: it is marked there.

As a business decision: automatic failover without a consensus layer and fencing of the faulty node trades a rare and recoverable outage for silent and permanent corruption. For anyone holding other people's financial data, that trade is bad in every scenario. The mitigation is not automating the promotion: it is keeping the replica hot and current, having the procedure written, and point-in-time recovery in place — all measured.

Two active-active nodes — and not one bigger node
resilience used every day, not asleep waiting for disaster
see trade-off
what I gain Both serve traffic all the time, so the resilience path is exercised on every request — not on the day of the disaster. It is also what lets me release on a node no user is accessing.
what I pay for it Everything has to be safe twice over: a scheduled task runs on both, so the protection against duplication lives in the database, not in a distributed lock — which would not even work, because the bus is local to each node.

As a business decision: idle standby is a cost that only pays off on the worst day — and that usually fails precisely then, because nobody exercises it. With both active, the same money buys capacity and resilience, and the proof it works is yesterday's traffic. Honesty about the limit: today both sit in the same availability domain, so a correlated domain failure still takes me down. It is marked in the diagram because it is the next step, not because I cannot see it.

In-house pipeline and quality gate
no hosted runner — with a consequence I say out loud
see trade-off
what I gain The suite runs against PostgreSQL with customer isolation armed, not against a toy database — a multi-tenant security defect shows up at the gate, not in production. No runner queue, no billed minutes, no production secret sitting in a third-party service.
what I pay for it There is no remote safety net. If I do not run the gate, nobody does — and I have already discovered it could finish green without having run a single test.

As a business decision: the value of the gate is not where it runs, it is what it rejects. A hosted runner executing the suite against a database with no customer isolation gives a feeling of coverage and lets through the class of defect that hurts most in a multi-tenant SaaS: leakage between customers. To offset the cost I took on: a test that watches the gate itself and fails loudly.

In-house identity and a single origin — and not outsourced identity
the decision that removes a whole class of attack instead of filtering it
see trade-off

The twelve verticals have no public address of their own. They are all mounted by path under a single origin, and the identity server — OAuth2 and key publication — lives on that same origin. Authorization does not sit only at a gate out front: it travels with the service, through a single SDK that every vertical carries and that denies the call when the scope is not present.

what I gain A single origin means a session on one domain only: no cross-origin sharing, no third-party cookie — which browsers are removing — and none of the surface a per-subdomain portal would create. And since authorization lives in the service, not only at the edge, anyone arriving from inside the network is blocked too: there is no “I already passed the gate, so now I am trusted”.
what I pay for it Identity is the most expensive component to operate responsibly: key rotation, expiry, federation. It has already cost me five and a half hours of login being down — I had rotated the signing keyset thinking it was the publishing one. Outsourcing would have avoided that specific incident.

As a business decision: the product is multi-tenant and sells to companies that need to inherit customer hierarchy, contracted plan and separation of duties. Outsourced identity charges per active user — the cost grows with the base at the exact moment the margin should improve — and pushes the business rule outside the system that owns it. Keeping it in-house makes the cost engineering (once) instead of subscription (always), and the who-sees-what rule stays where it is true. The cost is mitigated operationally: there is a dedicated alert for an expiring key and token, and the release gate validates that the key issuer responds before returning the node to traffic.

Four languages — and a single contract
polyglot by decision, not by historical accident
see trade-off

Java with Spring Boot in identity, accounting and the workflow engine. Go in the new services — a single binary, no runtime, starts fast and uses little memory, which is the scarce resource here. Rust in two precise places: the agent that observes the operating system core, and the tax calculation engine. Python with Django in the business verticals, where speed of change is worth more than microseconds.

what I gain Each problem in the tool that solves it best, without rewriting what already works. Since the contract between services is the event and the scope — not the language — swapping a service's implementation forces nobody else to change. That is what makes gradual migration possible, instead of a risky leap.
what I pay for it Four build chains, four sets of dependencies to keep updated and four ways to debug under pressure. For a small team that is real — which is why each language is confined to where it pays off, and not spread around for taste.

As a business decision: rewriting what works is the most expensive way to deliver nothing. The productive legacy keeps earning in Python while what is born is born in Go, and the stable contract between them is what allows swapping piece by piece without stopping the operation — without the risk of a total rewrite, which historically takes companies down.

One door to the world — and not a third-party SDK in every service
22 external integrations, a single place where they exist
see trade-off

Card acquirer, credit bureau, government APIs, open banking, e-signature, marketplace, messaging — everything comes in and goes out through a single service. It translates each vendor into a normalized capability and returns the result to the ecosystem as a fact on the bus. No business vertical knows the vendor's name; the business rule stays with the consumer, not with the adapter.

what I gain Switching vendor means changing one adapter, not hunting calls across twelve services. The third party's credential exists in a single place and comes from the vault. And the blast radius of an external failure stays contained: circuit breaker, retry with backoff and idempotency live there — a slow third party does not become a stuck queue inside the house.
what I pay for it One extra network hop on the path, and a service that, if it goes down, takes every integration with it at once. It is risk concentration — assumed, and that is why it is one of the most closely watched.

As a business decision: third-party integration is where contractual risk lives — a leaked credential, an improper charge, customer data leaving the premises. Concentrating means that auditing “who talks to the outside world, and with which credential?” is looking at one service, not twelve. And this is verifiable, not a promise: I measured the six business verticals and none has a third-party SDK installed — they only exist in the integration service. A rule that depends on human discipline fails silently; this one can be measured.

Cost as an architecture requirement — and not as a bill at month's end
the arithmetic that decides what goes in and what stays out
see trade-off

Operating alone with real customers imposes a constraint a large company does not have: every managed service I subscribe to is money every month and a part I do not control. That is why the design avoids the three classic sources of a bill that grows without warning: billed egress traffic, a balancer charged by the hour, and identity priced per active user.

what I gain Data leaves without a per-volume charge, because the backup goes to storage with no egress fee and traffic arrives through a tunnel rather than a metered balancer. In-house identity means a fixed engineering cost, not a subscription that grows with the user base — precisely when the margin should be improving. And the headroom is measured: 47% of memory used and load average below 1 on both nodes.
what I pay for it I pay in work. A managed service is expensive in money and cheap in time; my arrangement is the opposite. Every part I did not buy is a part I operate, patch and fix at 3 in the morning. That arithmetic only works while the scale fits on two machines and the operation fits in one person.

As a business decision: in a SaaS, infrastructure cost is what separates margin from loss as the base grows. The question I ask before adopting any service is “does this cost grow with the number of customers?” — if it grows linearly with revenue, it eats the economy of scale the model is supposed to provide. Right-sizing, declared CPU and concurrency limits and the release ledger exist as much for cost as for reliability: a resource nobody measures is a resource nobody cuts.

And the test almost nobody runs: how long does it take to come back?

A recovery plan that has never been timed is an intention, not a plan. These three numbers were genuinely measured, not estimated.

12 min
for the entire fleet to come up and converge
47 min
to provision a node from zero and have the fleet running on it
seconds
of maximum data loss — the WAL is archived continuously
  The 47 minutes are also the most honest proof of compute and application portability: it is the time to rebuild the compute and application layer on any provider that hands over a Linux machine — what runs is container images, PostgreSQL and Redis, with no proprietary service. It is not total independence — the edge, the image registry and the provider itself remain external dependencies, and replacing each one has its own cost.

“Nothing is released by hand — and nothing compiles on the production server.”

a rule born from two suspensions
IV

How it changes without falling over

Releasing is the moment a stable system becomes dangerous. The path below exists so that changing stops being an act of courage.

Continuous delivery

How a release happens

Nothing is released by hand and nothing compiles on the production server. The constraint was born out of real pain: the previous infrastructure was suspended twice for sustained CPU load — compiling on the host was one of the triggers. The architecture changed because of it.

1

Build outside production

The image is built on the workstation/CI and published to the registry with the commit tag. The production server only runs pull — it never compiles.

2

Drain the canary node from the balancer

The tunnel connector is stopped on node A. 100% of traffic goes to node B. The release happens on a node that no user is accessing.

3

Validate with real data — and with the right question

No crashloop, key issuer responding, and zero pending migrations. That last check was born from an incident: a process standing up does not mean the database kept up.

4

Failed? Automatic rollback by digest

It goes back to the previous digest on that same node and aborts — the stable node was never touched. And the tunnel connector is restored with verification: if that fails, it shouts on stderr with the manual fix command.

5

Passed? Back to the balancer, then mirror

The validated node returns to the tunnel and only then does the second node receive the same image, one service at a time. A global lock serializes the whole fleet: two simultaneous deploys once took the infrastructure down.

And it gets written down

Every recreated service is recorded in a ledger on the node with the date, the image digest and the commit. That is how you answer “what exactly is running?” without guessing.

The pipeline

The gate is local — and that is why it has teeth

The pipeline is in-house: there is no hosted runner checking the pull request. GitHub stores the code and serves as the image registry; the quality gate runs before that, on the workstation. This has an uncomfortable consequence I would rather say out loud: there is no remote safety net — if I do not run the gate, nobody runs it.

  What the gate demands

Formatting and lint with a ratchet (the floor only goes up), a project integrity check and the suite running on PostgreSQL with RLS armed — not on a toy database. Coverage below the floor fails. On the Java service, the same gate adds architecture tests, integration against real containers and static analysis.

  Nothing goes straight into the main line

Work happens on a branch, publishing goes through a pull request, and the main line is protected. push --force, reset --hard and deleting main are blocked by policy. Before a large refactor, a return marker.

  A green gate that never started

The gate target called the system interpreter and died right after the lint printed “all good”. Anyone reading the screen saw green on a gate that had not executed a single test. Today a test watches the gate itself — and it fails loudly.

“A system that never broke is a system nobody uses.”

three real incidents, and the scar each one left
V

When it broke — and what stayed

The part that normally does not get published. Cause, what failed in detection, what I made worse while trying to fix it, and the guard that prevents it from happening again.

Reliability

Incidents, and what changed afterwards

A system that never broke is a system nobody uses. What sets things apart is not the absence of incidents — it is what is left after one. These three are real, from the same day, and each one left a guard in the code.

Production went down

The migration that deadlocked against itself

Symptom
Every screen that read sales answered with an error: column does not exist. The release had been reported as complete.
Cause
The service uses two database roles — an owner, which runs DDL, and a runtime one, with RLS. A migration created the column in the owner's transaction and, on the next line, updated the data through the wrong role: the second connection could not see the column and sat waiting for the lock the migration itself was holding. Self-deadlock — and PostgreSQL does not even report it, because the owning session is not waiting on a lock, it is waiting on the client.
Why it slipped through
The suite runs with both roles collapsed into one. The defect only exists in production. And the deploy validation measured “the process came up”, not “the database kept up”.
I made it worse
My first attempt to apply the migration by hand died on timeout and left the session idle in transaction still holding the lock. I stacked two more attempts on top before I understood.
Resolution
Terminate the stuck sessions, roll back by pulling the previous image from the registry and recreate one node at a time. Then: reproduce the defect on a throwaway database — the broken version hangs, the fixed one applies — before publishing again.
What stayed
A structural AST guard in the suite (a data migration must use the migration's own connection) and a pending-migration count in the deploy, which aborts and rolls back.

The guard was born with the very vice it went hunting: it counted only the pending ones, and grep -c on empty output prints zero — identical to “none pending”. Today it counts the total too: an empty list is not a schema up to date, it is a lack of evidence.

Silent failure · money

The card discrepancy that was being swallowed

Symptom
None. That is the problem.
Cause
The daily reconciliation compares the acquirer's transactions with the recorded payments. Each discrepancy publishes an event — but the event type was never declared in the catalog, and the publisher refuses an undeclared type. The exception fell into a broad except wrapping the entire customer: every discrepancy was detected and discarded. A phantom transaction at the acquirer, or a payment without confirmation — money — became a log line.
How it surfaced
It did not surface: a new guard that sweeps the code for events published outside the catalog found it. It found two — this one and another.
The second defect
Declaring the event, on its own, would have fired one false alert per card sale: the production adapter cannot list the acquirer's transactions, and the code treated “I could not read” as “the acquirer has nothing”.
What stayed
With no read, the routine reports nothing and states explicitly that there was no evidence. An empty list coming from a successful read still counts as a real discrepancy.

Absence of measurement is not absence of the fact. And an alert that always screams blinds the watchman — it would have been worse than the original defect.

Integration dead in the middle

The event nobody was listening to

Symptom
Goods returned to the supplier left inventory correctly — and the debt to that supplier stayed intact. The store was paying for parts it had sent back.
Cause
The inventory service published the return event, with documentation literally saying that the financial document “lives in the other service”. No service consumed it. Green suite on both ends, feature dead in the middle.
And it could not be consumed
The event identified the supplier by the local identifier of whoever published it. On the other side, that identifier means nothing — and the resolver creates a generic supplier when it does not recognise the reference. Consuming it as it was would have created credit hanging on a phantom supplier.
What stayed
The event now carries the identity that crosses the boundary (central registry or tax document). Without it, nothing is published — and the screen warns the operator, through that same predicate, that the credit was not generated and what to do.

When measuring an integration, do not stop at “the event is published”. Ask who listens — and whether what it carries means anything on the other side.

Quality by construction

Guards as code

Every incident above became a test that fails the build if the class of defect returns. A convention written in a document ages; an executable guard does not. And every guard is tested with a mutant: if I reintroduce the defect and it stays green, it is worthless.

  One question, one owner

“Does this warehouse count as sellable stock?” was written, literally, in five places. With a second warehouse type, every forgotten copy becomes a silent hole. The rule now has a single owner — and a test fails anyone who writes it by hand.

  A published event is in the catalog

An AST sweep: every event type published in the code must be declared. It found two live cases — one of them was swallowing card discrepancies.

  A migration writes through the right connection

A structural guard, not a behavioural one: in tests the two database roles collapse, so the defect only exists in production. A behavioural test would stay green with the system on the floor.

  A symlink does not escape the tree

Two absolute links pointing at the machine of whoever created them ended up inside the production image — and they were self-referential: infinite recursion for any sweep that follows them.

SRE culture

It is not the title — it is what the practice forces on you

SRE, here, is not a title: it is a set of rules that already cost me dearly to learn and today are written into scripts, tests and policy. Six of them:

  Blameless post-mortems — including about me

The report of the first incident on this page has a field called “I made it worse”: my attempt at a manual fix stacked three stuck sessions before I understood the problem. A post-mortem that omits the author's own mistake does not help anyone learn — it protects the author.

  Restore first, understand later

With production on the floor, the move is to go back to the previous digest and only then investigate. The rollback is automatic and by digest — not by “rebuilding the earlier image”, which is not the same thing and has fooled me before.

  An alert that always screams blinds the watchman

An alert fired on a cumulative total stays lit forever and ends up ignored — so the new death arrives with nobody watching. An alert has to measure the change, and go quiet when the change stops.

  The constraint comes from pain, not from the manual

The previous infrastructure was suspended twice for sustained CPU load, and compiling on the server was one of the triggers; a third suspension would have been permanent. Today “production never compiles” is not a style preference — it is a rule with a review that blocks the pull request.

  Change is serialized, never parallel

Two simultaneous deploys once took the fleet down. There is a global lock: one service at a time, one node at a time, and an orphaned lock is reclaimed instead of jamming the fleet forever.

  Every change leaves a queryable trail

Every service recreation goes into a ledger on the node: date, digest and commit. The question “what exactly is running?” is answered by query, not by the memory of whoever deployed. That is where the measured average of 92 releases per day came from.

How I work

Three principles that came from scars

Measure before asserting

I once reported “it is operator configuration” about a field that was impossible to configure — the type could not hold the other service's identifier. Zero out of twelve records filled in across the whole fleet was not negligence: it was a defect. Today I measure before concluding.

Lack of evidence is not absence of a defect

A validation that could not measure cannot report “all good”. That confusion has shown up three times: in the deploy, in the card reconciliation and in a guard of my own — always wearing green.

A green gate is not a working feature

A suite with thousands of green tests coexisted with the main screen broken. A test with a stand-in does not measure the rule that only exists at the real boundary — and visual verification catches what no test catches.

  About this page. The incidents are real and the numbers were measured on the fleet. Customer names, network addresses and internal identifiers were left out on purpose — that is third-party information and attack surface. Anyone who wants to go deeper, I will demonstrate live.

“What I have not tested yet is worth as much as what I have — as long as I say which is which.”

the honest state of what is missing
VI

Capacity, continuity and governance

What is done, what is partial and what does not exist yet. Separating the three is what makes the first two columns trustworthy.

Capacity engineering

doneCPU, memory and concurrency limits declared per service in the composition file — not left at the tool's default
doneHeadroom measured: 47% of memory used and load average below 1 on both nodes
doneQueues segregated by nature, so slow work does not delay urgent work
partialLatency percentiles: measured. p50 50 ms · p95 96 ms · p99 100 ms, from the edge router's histogram, with zero 5xx responses in the period. The caveat that matters: this was at 0.43 req/s — it proves a healthy path, not resilience. The stress test, pushing to saturation to find the knee of the curve, is still not done
partialN-1 exercised in practice, dozens of times a day. The canary release drains one node from the balancer for ~200 s (median) per service: 82 windows in 72 h. During them the remaining node carried all the traffic with load between 0.13 and 2.5 on 8 vCPU and zero 5xx responses. The caveat: this is N-1 under normal traffic, not under peak — proving it under maximum load is the next exercise

Security engineering

doneInbound surface removed by design: firewall with SSH only, no application port open
doneIn-house identity, scope-based authorization in every service and secrets in a vault with one token per consumer
doneCustomer isolation enforced in the database, fail-closed — it does not depend on the application code getting it right
partialCredential rotation: there is an expiry alert and a procedure, but rotation is not automatic — and it has already cost me a login outage
not yetFormal threat modelling, a signed dependency inventory and continuous vulnerability management. There is dependency updating and review, but not a process with an artefact

Continuity and recovery

doneEncrypted backup off the provider, at three cadences: a dump every 3 h, a weekly base and continuous WAL
doneRestoration already exercised, and an alert that watches for the absence of a backup — not only for a backup failure
doneRTO timed and documented: 12 min for the fleet, 47 min for a node from zero — September 2026, on the current provider, with an end-to-end criterion (authenticate and get into the system). What is missing to make it evidence of portability: repeating it on a different provider
not yetA disaster exercise with real loss of the primary, timing the manual promotion end to end. Today's RPO is a replication-lag sample, not the result of a provoked failure

Governance

doneProtected main line, work on branches and publishing through review; destructive operations blocked by policy
doneRelease ledger: date, digest and commit of every service recreation
doneArchitecture decisions recorded with the cost each one imposes — that is Act III of this page
partialThe quality gate runs on the workstation, by manual execution. It is rigorous (real database, isolation armed, coverage that fails the build), but it is not an end-to-end automated pipeline: without remote execution on review, the safety net depends on me remembering
  Why publish the “not yet” column. Because the alternative is worse. Anyone assessing this kind of system will ask precisely about load testing, N-1 and disaster exercises — and discovering the gap in conversation is worth less than finding it written here, with the reason and the next step. A declared gap is a plan; a discovered gap is a surprise.

Want to see it running?

I can demonstrate a canary release end to end — including the automatic rollback when validation fails.

contact@magnosilva.com LinkedIn Voltar ao início