Expertise
Software architect: delivery is not a guarantee of the decision
In an invoicing flow the outbox kept a message from being lost but did not promise that the same message would produce the same outcome; I practise architecture as the sum of decisions that account for that second gap too.
- Since
- 2022 Since
- years
- 4 years
- posts
- 5 posts
- projects
- 10 projects
Me in this ecosystem
I practise architecture not as a collection of patterns but as decisions made in a particular system, with a particular boundary and a particular cost. This page covers three topics in its own right: Software Architecture, System Design and Event-Driven Architecture.
The evidence for all three sits on this site. The delivery and idempotency side is in the outbox log, how a legal constraint narrowed a design is in the number-collision log, and the determinism question that comes after delivery is in the determinism post. The roles themselves are on the resume, and the book-length version of the topic is Designing Reliable Event-Driven Systems. If you want to go deeper, the sade.dev pieces on why architecture decisions rot and on the outbox and idempotent consumption are the right place.
The three topics this page covers
Each rests on a post, a record or a role on this site; the page stays at the level of identity and evidence, not depth.
-
Software Architecture
It covers service boundaries, critical design decisions and passing them on to the team. In my Senior Software Developer and then Staff Engineer roles I ran architecture together with domain-driven design and test-driven development; writing boundaries into code and checking them in CI instead of leaving them on a wiki is part of this topic.
-
System Design
It covers a system's scaling, availability and load-distribution decisions. I moved a synchronous invoicing flow onto a shared queue and a separate consumer service. The groundwork for this came before architecture was my responsibility, in a search infrastructure, where I built a cache layer that used MySQL and Redis together, and split the database and cache clusters.
-
Event-Driven Architecture
It covers delivery, repetition and determinism. The transactional outbox, the idempotent consumer and at-least-once delivery are the practice of this topic, and so is the point that a delivered message producing the same outcome is a separate guarantee.
The cost of a decision
In e-invoice numbering a legal constraint became the real force behind the design. The chain is read in order.
- 01 Generating the number late caused a race
- Adding one to the maximum value at send time worked with a single worker; parallel workers produced the same number and reached the external API with it.
- 02 Generating the number early caused a gap
- If the number is generated when the message is queued, an invoice that drops out or is cancelled leaves an unexplained hole in the series. A legal series cannot skip a number.
- 03 The legal constraint narrowed the solution set
- Many methods solve the collision alone; once gap-freeness was added, the solution set narrowed at once. Before setting a requirement aside as a business rule, it is worth asking how it constrains the architecture.
- 04 The number was reserved just before the external call
- A reserved number stays on the invoice even if the call fails and is retried with the same number. The external call was kept outside the transaction; otherwise the system would have become a bottleneck.
Delivery is not a guarantee of the decision
Every guarantee comes with a cost; solving one pattern opens the next.
- 01 The outbox keeps a message from being lost
- The message is written as an outbox row in the same transaction as the record, and a relay publishes it. That closes the dual-write problem.
- 02 At-least-once brings repetition
- The message is not lost but may repeat. An idempotent consumer makes the repetition free of side effects.
- 03 A delivered message may not reach the same decision
- When the same prefix list was kept in two separate services, updating one let one of two same-shaped messages, a few hours apart, pass and the other be rejected. The message is not the whole input to the decision.
- 04 Context has to be frozen at creation time
- The consumer should not resolve what the decision needs on its own; the event should carry its own context. A retry must not change the moment of decision.
How I run architecture
Three habits, all aimed at letting a team read the same decision the same way.
-
Writing down the cost of a decision
I do not count a decision as architecture until I have written what it closes and what it puts in its place. The two chains above are the output of that habit.
-
Writing the boundary into code
Declaring layers and permitted dependencies in one file and checking them in CI on every commit keeps an architectural decision from rotting two years later.
-
Passing knowledge on to the team
I mentored the team in TDD and DDD and prepared a code-review guide; architecture lives not in the document but in the small decisions a team makes every day.
The depth is not here
This page stays at the level of identity and evidence. I write the pattern narratives, why architectural decisions rot, and the details of the outbox and number reservation on sade.dev.
Recent writing in this area
Same message, different outcome: determinism in event-driven architecture
Why does the same message produce different outcomes in an event-driven system? The hidden inputs that break determinism — and four moves to win it back.
Measurements in this area
One core carries 14,330 OAuth2 requests in Go and 5,152 in PHP-FPM
The same API verifies an RS256 bearer token on every request and then reads or writes one row in PostgreSQL — on one, two and four cores, how much mixed traffic does it carry in Go, PHP-FPM and FrankenPHP worker mode?
Finding
On four cores Go carried 57,321 mixed requests a second, FrankenPHP 25,659 and php-fpm 20,606 — 14,330, 6,415 and 5,152 per core. The number that goes into a capacity plan is not that one but application CPU per request: 66.8, 110.7 and 187.7 microseconds. At saturation FrankenPHP uses only 2.84 of its four cores, against Go's 3.83 and php-fpm's 3.87. The database is not the constraint: on the same four cores PostgreSQL alone writes 68,212 rows a second, above the mixed ceiling of the fastest candidate.
measured 21 days ago
The partial index grew three hundred and five times in fifteen minutes — and autovacuum never ran
Under sustained churn, does a partial index stay small on a queue table, and do the default autovacuum settings keep up with it?
Finding
With the live set holding at five thousand rows until about 845 seconds, the partial index went from 0.125 MB to 38.2 MB — three hundred and five times. Its smallness comes from the live set, its bloat rate comes from throughput, and nothing connects the two. The composite index bloated less in proportion (42%) and more in absolute terms (+126 MB), and while bloating it fell over — most likely because it no longer fit in memory, a cause this run did not measure: its latency went from 0.52 ms to 61 seconds and its backlog climbed to 126,000. Fifteen minutes produced 1.75 million dead rows and autovacuum **did not run once** — the default threshold scales with the whole table (50 + 0.2 × 10 million ≈ 2 million) while the churn happens in a tiny subset.
measured 47 days ago
Postgres never turned the partial index into a generic plan: forty executions, forty custom plans
Does Postgres switch a partial-index query to a generic plan on its own inside a prepared statement — or is the 1,635-fold cliff something you have to opt into?
Finding
Postgres declines. On the partial index all forty executions used a custom plan — the counter reads 40/0. The reason it declines is the disaster itself: a generic plan cannot use the partial index, so its estimated cost comes out high and the planner does not choose it. The composite index switches at the sixth execution exactly as documented (5/35) and loses nothing by it. So the 1,635-fold cliff is real but fenced: reaching it takes writing `plan_cache_mode = force_generic_plan`.
measured 47 days ago
What I have built in this area
Reaching production data without handing out the password
A self-hosted portal that puts ad-hoc production SQL behind approval, masking and an immutable trail; it became the QueryProxy product.
What it does today
Runs a developer's SQL against production through an approval step rather than directly; results are masked as they are written to disk and every request lands in an immutable record. Teams where production access sits with one person can run it today.
Personal and business finance on one data model
A finance core that keeps multi-account income and expense tracking in a single model; it became Parantaj, running on web, iOS and Android.
What it does today
One account/transaction model carries multi-account management, budgeting, goals and reporting on a single core; the web, iOS and Android clients are all live. Anyone who wants personal and business finances tracked in one place can use it.
Serving the documentation of dozens of projects from one server
A multi-tenant documentation server that gives every project its own MCP endpoint and keeps the documents on your own machine.
What it does today
Makes a project's folder, git repository, Obsidian vault or Notion workspace searchable behind a single MCP endpoint, so an agent reaches the documentation without the data going to a third party. Anyone who can run Docker can host it and manage it from the dashboard.
What shipped in this area
QueryProxy
OngoingFounder & Developer
Tier: Main FocusQueryProxy is a self-hosted query approval portal I built so developers can reach production data without ever holding production credentials. Submitted SQL is parsed and guarded, runs on a queue once a DBA approves it, is masked as the results are written to disk, and every step lands in an immutable audit record.
Parantaj
OngoingOwner
Tier: Active DevelopmentA personal and corporate financial management platform. It offers income-expense tracking, budget planning, detailed reporting, and multi-account management.
academia.sh
OngoingFounder & Author
Tier: Active Developmentacademia.sh is a learning platform with free, no-signup lessons that never separates a lesson's theory from a real problem from the field. It's presented through a curriculum-course-topic-lesson hierarchy, progress and quizzes are measured per lesson without splitting theory from problem, and full-text search runs on Postgres; the course-end exam, certificates and study assistant sit in a Pro tier. Content lives in a separate GitHub repo; the first field is computer science, and the model was designed multidisciplinary from the start.
Questions in this area
13 questions answered on this axis.
How do I position feature flags when moving from Gitflow to Trunk-Based?
Drop the long-lived branches, merge small changes to main continuously, keep unfinished work behind a flag that's off in prod, and delete flags once done.
When should I apply CQRS, and how do I sync the read and write models?
Move reporting to a read replica and adopt CQRS only when query shapes outgrow one schema, feeding the read model from outbox events, not dual-writes.
Should I choose NoSQL or PostgreSQL for flexible product attributes?
Keep products, prices and orders in PostgreSQL and put the variable attributes in one GIN-indexed `JSONB` column; reporting alone rules out splitting.
Technologies I pair it with
-
RabbitMQ
Asynchronous delivery between services; the exit door of the outbox relay.
-
Redis
The fast layer that works alongside the relational store in a cache layer.
-
MySQL
The store behind cached reads and the outbox table.
-
PostgreSQL
The database I measured queue-table and index behaviour on.
-
Docker
Development environment and deployment unit.
-
GitHub Actions
The CI pipeline where architectural boundaries are checked on every commit.
Frequently asked
5 questions
-
How long have you worked in architecture?
Since 2022, 4 years. I started as a Senior Software Developer and since March 2025 I have designed microservice and event-driven architecture as a Staff Engineer. The system-design groundwork from my earlier search-infrastructure years is the base this rests on, not part of that count. This site holds 5 posts that overlap with this work.
-
How do you tell Software Architecture, System Design and Event-Driven Architecture apart?
Software Architecture covers boundaries and decisions, System Design covers how a system scales and stays available, and Event-Driven Architecture covers delivery, repetition and determinism between services. They are different scales of the same work; this page holds all three.
-
Does moving work onto a queue give you a delivery guarantee?
No. The outbox keeps a message from being lost but gives you at-least-once; an idempotent consumer suppresses the repetition. A delivered message reaching the same decision is a third guarantee, and in an invoicing flow I had to build it separately.
-
How do you judge an architectural decision?
By what it closes and what it puts in its place. In the invoice-number case, generating the number late caused a race and generating it early caused a gap; the answer was to hold both at once.
-
Where can I read the deeper architecture writing?
On sade.dev. This page stays at the level of identity and evidence; the pattern narratives and longer architecture pieces are there.
Let us work together
If you have work in this ecosystem, tell me what you are building and we will talk about how to build it.