Skip to content
Muhammet Şafak
tr

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

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

See all : Recent writing in this area
Service & load Measurement

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

Medium confidence

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

High confidence

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

High confidence

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.

Open-Source Web PHP Laravel Livewire +5 more
September 2026 — September 2026

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.

Open-Source Web Mobile PHP Laravel Go +4 more
March 2024 — October 2024

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.

Open-Source Web TypeScript Node.js Fastify +6 more
August 2026 — September 2026
The QueryProxy approval queue — a pending query request, its guard warnings and masked result columns

QueryProxy

Ongoing

Founder & Developer

Tier: Main Focus

QueryProxy 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.

PHP Laravel Livewire +13 more
September 2026 — Ongoing
Parantaj cover — a finance dashboard with total balance, accounts, spending categories and a cash-flow chart

Parantaj

Ongoing

Owner

Tier: Active Development

A personal and corporate financial management platform. It offers income-expense tracking, budget planning, detailed reporting, and multi-account management.

PHP Laravel Go +8 more
October 2024 — Ongoing
academia.sh lesson screen — the curriculum/course/topic/lesson hierarchy, a panel showing theory and the field problem flowing in the same lesson text, full-text search and certificate field-freezing summaries

Founder & Author

Tier: Active Development

academia.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.

Laravel PHP Livewire +6 more
September 2026 — Ongoing

Questions in this area

13 questions answered on this axis.

All questions : Questions in this area

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.

Search the site

Start typing to search posts, projects and pages.

Esc to close Powered by Pagefind