<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Ahmad Almunajjed — Writing</title><description>I help startups, SMEs, and growing businesses ship software that holds up, modernize systems that no longer do, and train the teams that maintain them.</description><link>https://www.almunajjed.biz</link><language>en</language><item><title>What a team decides before its first microservice</title><link>https://www.almunajjed.biz/blog/before-your-first-microservice</link><guid isPermaLink="true">https://www.almunajjed.biz/blog/before-your-first-microservice</guid><description>Once splitting is justified, four decisions decide whether it works: where the boundary falls, who owns which data, what replaces the transaction, and what has to exist operationally before anything ships.</description><pubDate>Mon, 27 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Most writing about microservices argues about whether to adopt them. That argument
has an answer for small teams, and I have &lt;a href=&quot;/blog/modular-monolith-for-small-teams&quot;&gt;made it
elsewhere&lt;/a&gt;: a modular monolith gives you the
same boundaries without the distributed system, and the triggers for splitting
something out are specific enough to name.&lt;/p&gt;
&lt;p&gt;This is about what happens after one of those triggers actually fires. A second team
takes ownership, a component needs its own runtime, a compliance requirement forces
isolation — and now the split is justified. The failure mode from that point is not
choosing microservices. It is splitting along the wrong lines and discovering it
eighteen months later, when moving a boundary means a data migration plus a versioned
contract plus a coordinated rollout across two teams.&lt;/p&gt;
&lt;p&gt;Four decisions do most of that work. This is the material I train teams on when they ask
how to make the split without regretting it, and it was the substance of a five-day
course I delivered to a government engineering team, in Arabic, on designing and
implementing microservices with .NET.&lt;/p&gt;
&lt;h2 id=&quot;where-does-the-boundary-actually-fall&quot;&gt;Where does the boundary actually fall?&lt;/h2&gt;
&lt;p&gt;The boundary that survives is the one drawn around a &lt;strong&gt;business capability that owns
its own decisions&lt;/strong&gt; — not around a noun, and not around a layer.&lt;/p&gt;
&lt;p&gt;The noun trap is the common one. “Customer” looks like an obvious service until you
notice that sales, billing, and support each mean something different by it, need
different fields, and change on different schedules. A “customer service” that all
three call becomes a bottleneck that every team queues behind, which is the exact
problem splitting was supposed to solve.&lt;/p&gt;
&lt;p&gt;The test that works is about change rather than structure: &lt;strong&gt;if a single business
change routinely requires coordinated releases from two services, the boundary is in
the wrong place.&lt;/strong&gt; One new field in a checkout flow should not require three teams to
agree a deployment order.&lt;/p&gt;
&lt;p&gt;A useful smell in the opposite direction is a service that everything depends on and
that depends on nothing. That usually means the boundary was drawn around a data
structure rather than a capability — it is a shared database with an HTTP interface
in front of it.&lt;/p&gt;
&lt;h2 id=&quot;who-owns-which-data-once-it-is-across-a-network&quot;&gt;Who owns which data, once it is across a network?&lt;/h2&gt;
&lt;p&gt;Inside one process, “each module owns its tables” is a discipline. Across a network,
it is the whole design, because the moment a service needs data it does not own, you
have to pick how it gets it — and every option costs something:&lt;/p&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;Approach&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Consistency&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Failure behaviour&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Cost&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;Synchronous call to the owner&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Always current&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Caller fails when owner is down&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Coupled availability&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;Replicate via events&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Eventually consistent&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Caller keeps working, data is stale&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Must handle staleness explicitly&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;Shared database&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Always current&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;—&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Not a split at all&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The third row is the one teams reach for under deadline pressure, and it silently
undoes the entire exercise. Two services against one schema are one system with two
deployment pipelines: the coupling is still there, it is just no longer visible in
the code.&lt;/p&gt;
&lt;p&gt;The second row is usually right, and the decision it forces is the one teams skip:
&lt;strong&gt;how stale is acceptable, per field.&lt;/strong&gt; A product name being a few seconds out of date
is fine. A credit limit being a few seconds out of date is a business decision, not a
technical one — and it is exactly the kind of question that should be answered with
the business rather than assumed by an engineer at eleven at night.&lt;/p&gt;
&lt;h2 id=&quot;what-replaces-the-transaction&quot;&gt;What replaces the transaction?&lt;/h2&gt;
&lt;p&gt;This is the decision that surprises teams most, and it is the one that generates the
most production incidents when it is not made deliberately.&lt;/p&gt;
&lt;p&gt;Inside one process, a unit of work is one database transaction: it commits or it does
not. Split across services, “place an order” becomes reserve stock, then charge the
card, then create the shipment — three steps that can each fail independently, and
one of which involves money.&lt;/p&gt;
&lt;p&gt;There is no distributed transaction coming to save this. The realistic options are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Compensating actions.&lt;/strong&gt; Every step gets an explicit undo — release the
reservation, refund the charge. Straightforward to describe, and the compensations
are where the bugs live, because they are the paths that run least often.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Make forward progress the only direction.&lt;/strong&gt; The order is accepted in a pending
state and the system retries until each step succeeds, with a human queue for what
never does. Usually the better fit for commerce, because a customer would rather
wait than have their order silently vanish.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Both require &lt;strong&gt;idempotency at every step&lt;/strong&gt;, and that is not optional. A retried
“charge the card” that is not idempotent charges twice. In practice this means every
operation carries a caller-supplied key and the receiver records what it has already
done with that key — designed in from the first service, not added after the first
duplicate charge.&lt;/p&gt;
&lt;p&gt;The related decision is what the user sees meanwhile. A status that is derived from
the steps that have actually completed is honest and survives partial failure. A
status field that something remembers to update is a field that eventually disagrees
with reality.&lt;/p&gt;
&lt;h2 id=&quot;what-has-to-exist-before-the-first-service-ships&quot;&gt;What has to exist before the first service ships?&lt;/h2&gt;
&lt;p&gt;A modular monolith fails loudly and locally: one stack trace, one log stream, in
order. A distributed system fails quietly and somewhere else. The operational floor
is therefore part of the architecture, not a follow-up ticket:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Correlation across services.&lt;/strong&gt; One identifier, generated at the edge, present in
every log line and every outbound call. Without it, “this request was slow” is not
an answerable question — and adding it later means touching every service.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;One place to read the logs.&lt;/strong&gt; Logs sitting on five machines are five
investigations.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Health and readiness that mean something&lt;/strong&gt;, so a deployment fails on the branch
rather than in production.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A defined answer to “who gets paged”&lt;/strong&gt; for each service, agreed before the first
incident rather than during it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;None of this is individually difficult. It is constant, and it comes out of the same
budget as the product — which is the honest cost of the split, and the reason the
split should be triggered rather than chosen.&lt;/p&gt;
&lt;h2 id=&quot;the-order-these-get-decided&quot;&gt;The order these get decided&lt;/h2&gt;
&lt;p&gt;Boundaries first, because everything else follows from them. Then data ownership,
because it is what makes a boundary real rather than decorative. Then the transaction
question, because it decides what the system does when the network misbehaves — which
it will. Then the operational floor, because without it you cannot see any of the
above working.&lt;/p&gt;
&lt;p&gt;Teams usually do this in reverse: infrastructure first, because it is tangible and
there are tools to buy. That produces a well-monitored system with the boundaries in
the wrong place, and no amount of observability fixes that.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;This is the material I train teams on. If your team is at this decision, the course is built
around your system rather than a generic example — see
&lt;a href=&quot;/services/team-training&quot;&gt;team training&lt;/a&gt;, or read &lt;a href=&quot;/blog/modular-monolith-for-small-teams&quot;&gt;the case for not splitting
yet&lt;/a&gt; if the trigger has not actually fired.&lt;/p&gt;</content:encoded></item><item><title>Why a small team should start with a modular monolith</title><link>https://www.almunajjed.biz/blog/modular-monolith-for-small-teams</link><guid isPermaLink="true">https://www.almunajjed.biz/blog/modular-monolith-for-small-teams</guid><description>Microservices solve a coordination problem most small teams do not have yet. A modular monolith gives you the same boundaries without the distributed system.</description><pubDate>Sun, 26 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;!--
  Unpublished until Ahmad approves the text: set `draft: false` to ship it, and
  delete this comment when you do.

  Sourcing: written from stated expertise only (services.ts #06, &quot;Train teams:
  modular-monolith architecture, scalable systems, and practical engineering
  discipline&quot;) plus general engineering reasoning. It names no client, quotes no
  metric, and reports no specific project, because none have been supplied.
--&gt;
&lt;p&gt;Microservices answer a question about people rather than about code. Once enough teams
need to ship the same system without queueing behind each other, splitting the system
along team lines starts to pay for itself. A company with one team of five engineers is
a long way from that. Adopting the architecture anyway means paying the running costs of
a distributed system to solve a coordination problem the company does not have yet.&lt;/p&gt;
&lt;p&gt;The alternative is a single deployable application with real internal boundaries, which
is a different thing from the tangled codebase people picture when they hear “monolith”.&lt;/p&gt;
&lt;h2 id=&quot;what-a-modular-monolith-is&quot;&gt;What a modular monolith is&lt;/h2&gt;
&lt;p&gt;A modular monolith is one deployable application divided internally into modules that own
their data and expose a narrow public interface. Modules call each other in process,
through those interfaces, and never by reaching into each other’s tables. The boundaries
are the same ones you would draw between services. What you leave out is the network
between them.&lt;/p&gt;
&lt;h2 id=&quot;the-network-is-the-expensive-part&quot;&gt;The network is the expensive part&lt;/h2&gt;
&lt;p&gt;Once a call leaves the process it acquires failure modes that an in-process call does not
have. It can be slow, it can be retried, it can succeed on the server and fail on the
client, and it can arrive twice. Each of those needs handling code: timeouts, retries with
backoff, idempotency keys, circuit breakers, and something that reconciles state when the
retry lands anyway.&lt;/p&gt;
&lt;p&gt;Data is the same story. A unit of work that used to be one database commit becomes a
sequence of steps that can fail halfway through. You either accept that some states will
be temporarily wrong and design around it, or you write a compensating action for every
step. Both are real work, and both replace something a single transaction did for free.&lt;/p&gt;
&lt;p&gt;Then there is everything around the code. Each service needs its own pipeline, its own
configuration, its own alerting, its own place in whatever traces a request end to end,
and its own answer to the question of who gets paged. Four engineers running six services
own six of each. None of it is individually difficult. It is constant, and it comes out of
the same budget as the product.&lt;/p&gt;
&lt;h2 id=&quot;what-one-process-buys-you&quot;&gt;What one process buys you&lt;/h2&gt;
&lt;p&gt;Staying in one process costs a small team almost nothing, because nobody is waiting on
somebody else’s release. In exchange:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A call between modules is a function call. It cannot time out, arrive twice, or
half-succeed.&lt;/li&gt;
&lt;li&gt;A unit of work is one database transaction. It commits or it does not.&lt;/li&gt;
&lt;li&gt;A request produces one stack trace and one log stream, in order.&lt;/li&gt;
&lt;li&gt;A release is one artifact through one pipeline.&lt;/li&gt;
&lt;li&gt;A boundary you drew in the wrong place is a refactor, rather than a data migration plus
a versioned contract plus a coordinated rollout.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That last point is the one people underestimate. Early on, the boundaries are guesses.
Being able to move them cheaply is worth more than being able to deploy them separately.&lt;/p&gt;
&lt;h2 id=&quot;drawing-the-boundaries&quot;&gt;Drawing the boundaries&lt;/h2&gt;
&lt;p&gt;The boundaries are the part worth the effort, and they are the same whether you deploy
once or twenty times.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cut along business capabilities, not technical layers.&lt;/strong&gt; Ordering, inventory, billing,
and notifications are modules. Controllers, services, and repositories are not. A layered
cut produces a codebase where every feature touches every folder, which is the version of
“monolith” everyone is afraid of.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Give each table exactly one owner.&lt;/strong&gt; A module owns its data and no other module reads it
directly. When ordering needs a stock level it asks the inventory module. This single rule
does most of the work, because shared tables are the thing that makes a system impossible
to separate later, and they are cheap to add and expensive to undo.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep the public surface small and explicit.&lt;/strong&gt; A module exposes an interface and a set of
types. Everything else is internal. Most languages can enforce that with their package or
namespace system, and where the language cannot, a build check can.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Use in-process events for anything that is not the caller’s business.&lt;/strong&gt; When one module
needs to react to something another module did, publishing an event usually fits better
than a direct call. Ordering does not need to know that notifications exist.&lt;/p&gt;
&lt;h2 id=&quot;keeping-the-boundaries-honest&quot;&gt;Keeping the boundaries honest&lt;/h2&gt;
&lt;p&gt;Boundaries decay under deadline pressure unless something enforces them. A build-time
dependency rule that fails when a module imports another module’s internals is worth more
than any diagram, because it fails on the branch instead of at the next architecture
review.&lt;/p&gt;
&lt;p&gt;Two failure patterns are worth watching for. The first is the shared &lt;code&gt;common&lt;/code&gt; package,
which starts life holding a date helper and ends up holding half the domain. If something
is used by two modules, it probably belongs to one of them. The second is a module that
everything depends on and that depends on nothing. That usually means the boundary was
drawn around a data structure rather than a capability.&lt;/p&gt;
&lt;h2 id=&quot;when-to-actually-split-something-out&quot;&gt;When to actually split something out&lt;/h2&gt;
&lt;p&gt;Extraction is a decision with a trigger. The triggers are specific:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A part of the system needs different hardware or a different runtime from the rest.
Image processing, model inference, and long running jobs are the usual candidates.&lt;/li&gt;
&lt;li&gt;A part needs to scale on its own axis, and scaling the whole application to serve it
costs more than running it separately.&lt;/li&gt;
&lt;li&gt;A second team takes ownership of something and the shared pipeline becomes the queue
everyone waits in.&lt;/li&gt;
&lt;li&gt;A compliance or data residency requirement forces isolation.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;“We might need to scale one day” is not on that list. When one of the real triggers does
fire, a module that already owns its data and exposes a narrow interface is most of the
way to being a service: the interface becomes the API, the tables move with it, and the
callers change one import.&lt;/p&gt;
&lt;p&gt;That is the practical argument for building this way. The work that keeps a monolith
modular is the same work that makes it splittable, so you get the option without paying
for it up front.&lt;/p&gt;
&lt;p&gt;This is the architecture I train teams on when they ask how to structure a system they will
still be able to change in three years. The reason is unglamorous. For a team with more
product to build than infrastructure to run, it keeps the most decisions open at the
lowest running cost.&lt;/p&gt;
&lt;p&gt;When one of those triggers does fire, the split has its own set of decisions —
where the boundary falls, who owns which data across a network, what replaces the
single transaction, and what has to exist operationally before anything ships. Those
are the subject of &lt;a href=&quot;/blog/before-your-first-microservice&quot;&gt;what a team decides before its first
microservice&lt;/a&gt;, which picks up where this post
ends. Both are material I cover in &lt;a href=&quot;/services/team-training&quot;&gt;team training&lt;/a&gt;.&lt;/p&gt;</content:encoded></item></channel></rss>