<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sergiy Yevtushenko</title>
    <description>The latest articles on DEV Community by Sergiy Yevtushenko (@siy).</description>
    <link>https://dev.to/siy</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F145374%2F6e93547b-45cc-430e-9659-3adea03266b5.jpeg</url>
      <title>DEV Community: Sergiy Yevtushenko</title>
      <link>https://dev.to/siy</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/siy"/>
    <language>en</language>
    <item>
      <title>Engineering Is the Checkable Fraction of Your Practice</title>
      <dc:creator>Sergiy Yevtushenko</dc:creator>
      <pubDate>Mon, 10 Aug 2026 21:43:20 +0000</pubDate>
      <link>https://dev.to/siy/engineering-is-the-checkable-fraction-of-your-practice-328m</link>
      <guid>https://dev.to/siy/engineering-is-the-checkable-fraction-of-your-practice-328m</guid>
      <description>&lt;p&gt;&lt;strong&gt;Craft externalizes nothing and transfers by apprenticeship. Engineering writes the governing relation down where someone else can find it wrong.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;Four times now I have written the same three sentences in different notations, for four problems that looked unrelated: a design method, a coding technology, an architecture-derivation procedure, and a contract-modelling tool. I noticed the repetition only after the fourth. Here it is, stated as precisely as I can manage.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Structure is derived from the attribution of forced change. The attribution is kept as an explicit, checkable artifact. The derivation refuses rather than guesses when its inputs underdetermine the answer.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Three clauses, each carrying weight. Drop one and look at what remains.&lt;/p&gt;

&lt;p&gt;Drop &lt;strong&gt;derived&lt;/strong&gt; and you have a documentation exercise: the structure was chosen first and the attribution written to match. This is the normal case, and it makes no prediction, so nothing can disagree with it.&lt;/p&gt;

&lt;p&gt;Drop &lt;strong&gt;explicit artifact&lt;/strong&gt; and you have taste. Real, valuable, and transferable only by apprenticeship.&lt;/p&gt;

&lt;p&gt;Drop &lt;strong&gt;refusal&lt;/strong&gt; and you have a generator that answers every question. Its answers carry no information, because it was always going to produce one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The third clause has a lineage worth claiming
&lt;/h2&gt;

&lt;p&gt;Type inference has refused for fifty years. Hindley-Milner unification fails rather than picking a plausible substitution: when two types cannot be reconciled, the answer is an error, not a guess.&lt;br&gt;
Core HM needs no annotations at all -- it infers principal types, and that is the point. The interesting part is what happened when later extensions broke that guarantee. Type classes admit programs whose type is inferable while the instance to use is not; GADTs and polymorphic recursion break principality outright. In each case the compilers were free to pick a plausible candidate, and they demand an annotation instead. Build systems joined later: Bazel refuses an undeclared dependency rather than resolving it from ambient state (&lt;code&gt;this rule is missing dependency declarations&lt;/code&gt;), and Nix in pure evaluation mode will not fetch what its inputs do not pin (&lt;code&gt;in pure evaluation mode, 'fetchTree' will not fetch unlocked input&lt;/code&gt;), with &lt;code&gt;--impure&lt;/code&gt; as the deliberate opt-out. In each case the refusal is the feature. It is what makes the successful output mean something.&lt;/p&gt;

&lt;p&gt;Decomposition methods have never had this, as far as I know. Layered, hexagonal, C4, arc42, the domain modelling schools -- feed any of them an underspecified system and you get a diagram. I know of none that returns &lt;em&gt;this does not determine a boundary&lt;/em&gt; for any input.&lt;/p&gt;

&lt;p&gt;So the move is a transplant, not an invention: take a discipline that inference engines and build systems have proven out, and apply it to structure. That is a smaller claim than novelty and a more useful one, because it means the hard part already has fifty years of evidence behind it.&lt;/p&gt;

&lt;p&gt;There is a plausible reason the shape keeps recurring without anyone coordinating it. The refusal aesthetic is ambient in typed functional programming -- parse don't validate, total functions over partial ones, illegal states unrepresentable, results over exceptions. What follows may simply be that tradition's core move, lifted from values to structure.&lt;/p&gt;
&lt;h2&gt;
  
  
  What it forbids
&lt;/h2&gt;

&lt;p&gt;A principle that forbids nothing is decoration. This one rules out five things concretely enough to check on a Monday morning:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Grouping by similarity or type.&lt;/strong&gt; All the &lt;code&gt;*Service&lt;/code&gt; classes in one package, all the DTOs in another. Similarity is not an attribution of change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"We needed coverage" as a test's reason for existing.&lt;/strong&gt; A test names the property it pins, or it does not get written.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Emitting a value the inputs do not determine.&lt;/strong&gt; No defaults dressed as derivations, no "approximately this" where the honest output is a refusal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A structure whose reason is recorded nowhere.&lt;/strong&gt; If the attribution lives only in the head of whoever drew the boundary, no one else can check it, and it leaves when they do.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A decomposition no history could contradict.&lt;/strong&gt; If nothing observable could disagree with your module boundaries, they assert nothing.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If a claimed application of this forbids none of the five, it is not an application of it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Where the shape showed up
&lt;/h2&gt;

&lt;p&gt;Each of these stands on its own argument, worked out in full at the links. What they have in common is the third clause -- each one has an output that is not an answer.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://pragmatica.dev/method/pfd/" rel="noopener noreferrer"&gt;Process-First Design&lt;/a&gt; decomposes by change driver, and will not draw a boundary when the drivers are unknown.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://pragmatica.dev/java/jbct/" rel="noopener noreferrer"&gt;Java Backend Coding Technology&lt;/a&gt; shapes test suites by the code property under test, and will not write a test whose only justification is coverage.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://pragmatica.dev/method/architecture-synthesis/" rel="noopener noreferrer"&gt;Architecture Synthesis&lt;/a&gt; returns &lt;em&gt;underdetermined&lt;/em&gt; instead of a recommendation when the inputs do not support one.&lt;/li&gt;
&lt;li&gt;A contract-modelling tool, built recently by someone working from the question "as a contract artifact, what could be better?" and almost none of the above, reports every fidelity loss rather than approximating over it. The question steered the axis; the refusal-shaped decisions were unprompted.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  What makes it checkable
&lt;/h2&gt;

&lt;p&gt;"Forced change" earns its place because it has an observable proxy. Version control records which files change together whether or not anyone was paying attention, so a decomposition can be compared against history and found to disagree. The measurement is neither new nor mine: logical coupling has been studied in the mining-software-repositories literature since Gall in 1998 and Zimmermann in 2004, and Adam Tornhill later productized it as change coupling in CodeScene. So this half is measurable today, with two decades of literature behind it and a tool that computes it off the shelf. What that buys is availability, not proof -- how well the metric predicts&lt;br&gt;
maintenance cost is still a live research question.&lt;/p&gt;

&lt;p&gt;The comparison has to be set up correctly to mean anything, and the reason is interesting in its own right: &lt;strong&gt;co-change is partly endogenous.&lt;/strong&gt; Files change together partly because the current&lt;br&gt;
structure forces them to, so raw co-change is downstream of the decomposition it is being used to check. Two adjustments make the instrument sharp:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Measure cross-boundary co-change.&lt;/strong&gt; Coupling that crosses a boundary indicates misattribution regardless of whether it was forced or induced.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use restructuring events as natural experiments.&lt;/strong&gt; Co-change that survives a known restructure is real. Co-change that disappears was an artifact of the old structure.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Why this is the engineering question
&lt;/h2&gt;

&lt;p&gt;Vincenti's &lt;em&gt;What Engineers Know and How They Know It&lt;/em&gt; divides engineering knowledge into the explicit -- descriptive and prescriptive -- and the tacit. He is arguing something else with that&lt;br&gt;
taxonomy, that engineering knowledge is its own thing rather than applied science, so the reading that follows is mine rather than his. Rigor is not what separates engineering from craft. Master craftsmen are rigorous. What separates them is whether the knowledge governing a decision is externalized into an artifact that someone other than the maker can check. A stress calculation can be audited by an engineer who was not present. A mason's judgment transfers only by apprenticeship.&lt;/p&gt;

&lt;p&gt;That is the second clause almost verbatim, and the first matches too: engineering derives dimensions from load cases, craft selects proportions from precedent and eye.&lt;/p&gt;

&lt;p&gt;It also explains something otherwise puzzling. Fifty years of calling this work "software engineering" did not make it engineering, and the usual diagnoses -- insufficient process, measurement, or professionalism -- do not fit, because all three have been tried at length. The explanation that does fit is that the governing relations stayed tacit. Nothing was externalized, so nothing could be audited, and nothing could be wrong.&lt;/p&gt;

&lt;p&gt;Which gives a definition worth using, stated as a ratio rather than a binary:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Engineering is the checkable fraction of your practice.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Craft is the limit at zero. Engineering-as-aspiration is the limit at one. Every real practice sits in between, and the same repository can be at both ends at once -- one of mine audited as engineering on its import path and craft on its export path, simultaneously.&lt;/p&gt;
&lt;h2&gt;
  
  
  The daily form
&lt;/h2&gt;

&lt;p&gt;Three clauses at project scale need a version that survives a Tuesday. Three questions at the moment of a decision:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;What relation governs this?&lt;/strong&gt; What would force this to change? What property is this test pinning? What does this boundary protect?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is that relation written down where it could contradict me later?&lt;/strong&gt; A commit message, a decision line, a test name. The artifact clause at whatever grain the moment affords.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Do my inputs actually determine the answer?&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Question 3 has a daily form distinct from its project form. You usually cannot refuse -- deadlines exist, most decisions are underdetermined, and they get made anyway. So the working version is&lt;br&gt;
never dressing a guess as a derivation: record it as a guess, with what would settle it.&lt;/p&gt;

&lt;p&gt;Two conditions keep this from becoming ceremony, which is how good disciplines die. Apply it only where a checkable relation exists; forced onto naming debates and aesthetic choices it degenerates into ritual. And "this is taste, and I am marking it as taste" is a valid answer to question 3.&lt;/p&gt;
&lt;h2&gt;
  
  
  The smallest instance
&lt;/h2&gt;

&lt;p&gt;Everything above is project-scale. Here is the same thing at the scale of one working session, which is where the day actually goes.&lt;/p&gt;

&lt;p&gt;I keep a &lt;code&gt;CLAUDE.md&lt;/code&gt; -- a file the coding agent reads before every session. That file is a governing relation, externalized into an artifact, constraining something whose output can then be checked against it. Which makes it an instance of the second clause rather than an illustration of one. Two of its lines carry the formulation directly, quoted as they stand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- **Record what governs a decision where it can later contradict you.** The driver behind a boundary, the property a test pins, the reason a default was chosen — at whatever grain the moment affords: a commit message, a test name, one line in the response. Never a new document unless asked, and only where a real relation exists; forced onto naming and aesthetic choices it becomes ritual.

**The third branch — proceed on a marked guess.** Ask and Execute are not the only moves, and most decisions are underdetermined yet still have to be made. When the inputs do not determine the answer but the work must move, neither block nor silently pick: proceed, say plainly which choice was a guess, and name what would settle it. Dressing a guess as a derivation is the failure; making one deliberately and saying so is not. "This is taste, and I am marking it as taste" is a complete answer.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first is the artifact clause with its anti-ceremony guard welded on, because without the guard an agent told to keep a record will generate decision documents nobody asked for. The second is the refusal clause adapted to something that cannot refuse: an agent asked to build will build, so the working form is not silence but a guess that is labelled as one.&lt;/p&gt;

&lt;p&gt;Neither line is the argument. They are the cheapest place to start on it -- one file, two paragraphs, and within a week you know whether it changed anything you do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the governing relation is not change
&lt;/h2&gt;

&lt;p&gt;The formulation says structure follows &lt;em&gt;forced change&lt;/em&gt;, which raises an obvious and genuinely open question: which domains have a different governing relation? Three where I think the answer is clearly yes:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data-oriented design.&lt;/strong&gt; It deliberately shreds change-cohesion in favor of memory access patterns, and is right to; Mike Acton's position is that every software problem is a data problem. Entity-component systems are one form of it, though practitioners are quick to add that you probably should not go build one. The governing relation there is machine sympathy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Security boundaries.&lt;/strong&gt; A threat model can require splitting things that change together, where merging them by change attribution would be the vulnerability itself. The relation is the trust boundary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cross-cutting concerns.&lt;/strong&gt; Telemetry co-changes with everything, so deriving structure from that attribution yields either nothing or aspect-oriented programming.&lt;/p&gt;

&lt;p&gt;The interesting question is whether these are exceptions to the first clause or instances of a broader one -- make &lt;em&gt;whatever relation governs&lt;/em&gt; explicit, derive from it, refuse when it underdetermines. I am not ready to claim the broader version, because every instance I have is a change relation and one sentence of generalization is easy to write and hard to earn.&lt;/p&gt;

&lt;p&gt;If you work in one of these domains, or a fourth, the specific case is what I want: &lt;a href="https://pragmatica.dev/method/counterexamples/" rel="noopener noreferrer"&gt;/method/counterexamples/&lt;/a&gt; takes one system, one derivation, and the place where the answer and reality parted ways.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lineage
&lt;/h2&gt;

&lt;p&gt;One ancestor is worth naming: Parnas, 1972, &lt;em&gt;On the Criteria To Be Used in Decomposing Systems into Modules&lt;/em&gt;. Parnas decomposed by design decisions likely to change -- concrete, and checkable in principle. What the three clauses add is that the attribution survives as an object after the decomposition is made, and that the derivation is allowed to decline.&lt;/p&gt;

&lt;p&gt;Parnas gave the criterion. Keeping it around afterwards is what makes it possible to find out you were wrong.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>softwaredesign</category>
      <category>programming</category>
      <category>engineering</category>
    </item>
    <item>
      <title>Keep the Context Map. Replace the Aggregates.</title>
      <dc:creator>Sergiy Yevtushenko</dc:creator>
      <pubDate>Fri, 07 Aug 2026 22:04:46 +0000</pubDate>
      <link>https://dev.to/siy/keep-the-context-map-replace-the-aggregates-1fj8</link>
      <guid>https://dev.to/siy/keep-the-context-map-replace-the-aggregates-1fj8</guid>
      <description>&lt;h1&gt;
  
  
  Keep the Context Map. Replace the Aggregates.
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Strategic DDD is nearly orthogonal to tactical DDD, so you can take one and swap the other&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;Domain-Driven Design ships as one book, and most people treat it as one method. It is two.&lt;/p&gt;

&lt;p&gt;The strategic half -- bounded contexts, context maps, core and supporting and generic subdomains -- is about &lt;em&gt;where the boundaries in a business are&lt;/em&gt;. The tactical half -- aggregates, entities, value objects, repositories -- is about &lt;em&gt;how you build inside one&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Notice what does not connect them. Nothing in a context map presupposes an aggregate. Nothing in an aggregate presupposes a context map. You can draw the first without committing to the second, and teams do it constantly in both directions: shops that map their contexts carefully and then build anemic services inside them, shops that build textbook aggregates with no map above them at all.&lt;/p&gt;

&lt;p&gt;The halves come apart. And that has a consequence people rarely take: &lt;strong&gt;you can keep one and replace the other.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This matters because the contested half is the tactical one. Arguments about DDD are almost always arguments about aggregates -- their size, their transactional boundary, whether they belong in an application with a relational database underneath. Very few people argue that bounded contexts are a bad idea.&lt;/p&gt;

&lt;p&gt;So here is the proposition. Keep the context map. It is doing real work and nothing below it depends on how you decompose inside a context. Then, inside each context, decompose by change driver rather than by aggregate.&lt;/p&gt;

&lt;p&gt;The rest of this is what that requires -- three annotations on a map you already have -- and what it buys.&lt;/p&gt;




&lt;h2&gt;
  
  
  What you need to know about the other half
&lt;/h2&gt;

&lt;p&gt;Process-First Design is a decomposition method for the inside of a boundary. Four ideas carry the whole argument here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A change driver is a reason code changes&lt;/strong&gt; -- a force that, when it moves, forces the code to move with it. Not a feature, not a module: a &lt;em&gt;reason&lt;/em&gt;. A pricing policy is a driver. A regulator is a driver. A storage decision is a driver.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You find drivers by asking who would ask for this to change.&lt;/strong&gt; Each independent decision authority is a driver: a team, a regulator, an external partner on its own release schedule. Two independent authorities able to demand changes to one unit means two drivers, and a split.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The artifact is a register&lt;/strong&gt; -- a plain table, use case against driver. It does triple duty: it is the grouping itself; it is a completeness-and-purity checklist, asking whether a driver's column holds all and only its use cases; and it is checkable against version&lt;br&gt;
control, because use cases that share a driver should change together in the commits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cohesion is the test.&lt;/strong&gt; Does one change force all of these, and only these? Everything inside a unit should change for the same reason, and nothing outside should change for that reason.&lt;/p&gt;

&lt;p&gt;There is no aggregate in that list. Decomposition falls out of driver attribution, and data is the residue of the processes rather than the thing you model first. That is where PFD and tactical DDD genuinely conflict, and it is the only place they do.&lt;/p&gt;

&lt;p&gt;One more thing, and it is why this article exists: &lt;strong&gt;PFD stops below the enterprise, on purpose.&lt;/strong&gt; The level above a system is composed by forces the code does not express -- Conway's law, the shape of the organization, who funds what. PFD does not claim that level. It consumes the strategic framing from wherever the enterprise already produces it.&lt;/p&gt;

&lt;p&gt;Strategic DDD is the best-known way to produce it. So the two are not competitors. One produces what the other consumes -- once you add three things the map does not currently carry.&lt;/p&gt;




&lt;h2&gt;
  
  
  Annotation 1: the authority behind each context
&lt;/h2&gt;

&lt;p&gt;A context map records the relationships &lt;em&gt;between&lt;/em&gt; contexts. It does not systematically record who can demand a change &lt;em&gt;inside&lt;/em&gt; one.&lt;/p&gt;

&lt;p&gt;That is the question the whole driver method runs on, so write it down. For each context: &lt;strong&gt;which independent authorities can demand change here?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Be strict about what counts. An authority is an agent that can require you to move: a team, a regulator, a vendor, a compliance regime, a named stakeholder. It is not a &lt;em&gt;concern&lt;/em&gt; -- "pricing" is not an authority, the pricing team is. This distinction is easy to lose, and losing it is what makes the next two annotations impossible.&lt;/p&gt;

&lt;p&gt;Written down, most contexts turn out to have two or three authorities, not one. That is already useful: a context with four independent authorities is a context that will fork, and you have found it before it does.&lt;/p&gt;




&lt;h2&gt;
  
  
  Annotation 2: cadence
&lt;/h2&gt;

&lt;p&gt;For each authority: &lt;strong&gt;how often does it actually demand change?&lt;/strong&gt; Weekly, quarterly, annually, when the law changes, never in five years.&lt;/p&gt;

&lt;p&gt;You can measure this rather than guess it. The commits are right there.&lt;/p&gt;

&lt;p&gt;Cadence buys two things. The first is that &lt;strong&gt;core, supporting and generic stop being a judgment call.&lt;/strong&gt; They fall out of two axes you are now tracking:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Business-owned, and moves fast -- &lt;strong&gt;core&lt;/strong&gt;. Build it, staff it, keep it inside.&lt;/li&gt;
&lt;li&gt;Externally owned, moves slowly -- &lt;strong&gt;generic&lt;/strong&gt;. Buy or conform; it is somebody else's problem and they change it rarely.&lt;/li&gt;
&lt;li&gt;Business-owned, moves slowly -- &lt;strong&gt;supporting&lt;/strong&gt;. Build it once, cheaply, and stop thinking about it.&lt;/li&gt;
&lt;li&gt;Externally owned, and moves fast -- &lt;strong&gt;generic, but defend it&lt;/strong&gt;. A vendor API on its own aggressive release schedule is the painful cell, and it is the one that most needs an anti-corruption layer, because the churn is real and none of it is yours.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last quadrant is usually missing from the discussion, and it is the one that hurts.&lt;/p&gt;

&lt;p&gt;The second thing cadence buys: it tells you where release independence is genuinely demanded rather than assumed. Two contexts owned by different teams that both ship on the same weekly train do not need separate deployables. Cadence &lt;em&gt;divergence&lt;/em&gt; is what forces that, not the count of teams.&lt;/p&gt;




&lt;h2&gt;
  
  
  Annotation 3: what actually crosses
&lt;/h2&gt;

&lt;p&gt;Context maps name relationships -- conformist, customer/supplier, shared kernel, published language, anti-corruption layer. Underneath each one, something concrete crosses the boundary: an id, a status enum, the value type of a field both sides touch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Write down the primitives, not just the relationship.&lt;/strong&gt; That list is the coupling inventory, and it is what the decomposition inside each context has to respect.&lt;/p&gt;

&lt;p&gt;The relationship names then read as a taxonomy of power over change, which is what they were always describing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Conformist&lt;/strong&gt; -- the foreign authority governs the shared primitive. You have no seat.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anti-corruption layer&lt;/strong&gt; -- a refusal to let a foreign driver propagate into your register. The ACL is where you absorb their cadence so it does not become yours.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared kernel&lt;/strong&gt; -- a genuinely joint primitive under joint authority. Rare, expensive, and worth confirming that the authority really is joint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Published language&lt;/strong&gt; -- a shared primitive whose owner has committed to a cadence. That commitment is the whole difference between this and conformist.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Read that way, the map is already about authority and cadence. The annotations do not add a new idea. They make explicit what the relationship names were compressing.&lt;/p&gt;




&lt;h2&gt;
  
  
  What you have when you are done
&lt;/h2&gt;

&lt;p&gt;A context map with an authority set and a cadence per context, and a primitive inventory per crossing.&lt;/p&gt;

&lt;p&gt;That is a PFD input. Inside each context, the authorities are your candidate drivers, you build the register by attributing use cases to them, and the crossing primitives are the coupling you are not allowed to dissolve because it is real. The decomposition proceeds from there, and no aggregate is required at any point.&lt;/p&gt;

&lt;p&gt;What you gain over carrying on into tactical DDD is a boundary that is checkable. The register can be tested against version-control history: use cases that share a driver should change together in the commits. A context map cannot be tested against anything, and neither can an aggregate.&lt;/p&gt;

&lt;p&gt;What you keep is everything strategic DDD gave you. The map, the relationships, the subdomain classification, the language. None of it is invalidated by swapping the tactical half, because none of it rested on the tactical half.&lt;/p&gt;




&lt;h2&gt;
  
  
  This has not been tested at scale
&lt;/h2&gt;

&lt;p&gt;I have not run this on a large multi-team system, because I do not have one and, as far as I know, neither does anybody else yet. Every methodology passes through this stage; saying so is not a disclaimer, it is a request.&lt;/p&gt;

&lt;p&gt;Here is exactly what would falsify it, so you do not have to take my word for any of it:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build the register, then compare it against the org chart.&lt;/strong&gt; If the derivation holds, driver boundaries and team boundaries should mostly coincide, and where they diverge, you should be able to point at real cross-team coordination for a single change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Then compare the register against the commits.&lt;/strong&gt; Use cases attributed to the same driver should change together. Files that change together but sit in different contexts mean a driver is cutting across a boundary the map says is there.&lt;/p&gt;

&lt;p&gt;Both are measurable, and both can come out against me. If you run either on a system you own and the boundaries do not line up, that is the interesting result, and I would like to hear about it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The decomposition method summarized here is described in full in&lt;br&gt;
&lt;a href="https://pragmatica.dev/method/pfd/" rel="noopener noreferrer"&gt;Process-First Design&lt;/a&gt;, including the register, the cohesion test, and the scope bound that puts the enterprise deliberately out of reach.&lt;/em&gt; &lt;/p&gt;

</description>
      <category>ddd</category>
      <category>architecture</category>
      <category>softwaredesign</category>
      <category>programming</category>
    </item>
    <item>
      <title>Every Test Can Name Why It Exists</title>
      <dc:creator>Sergiy Yevtushenko</dc:creator>
      <pubDate>Fri, 07 Aug 2026 20:39:50 +0000</pubDate>
      <link>https://dev.to/siy/every-test-can-name-why-it-exists-24bo</link>
      <guid>https://dev.to/siy/every-test-can-name-why-it-exists-24bo</guid>
      <description>&lt;h1&gt;
  
  
  Every Test Can Name Why It Exists
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Four kinds of test, each derived from a property of the code&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;Most test strategies arrive as a shape. The pyramid: lots of unit tests, fewer integration&lt;br&gt;
tests, a handful of end-to-end. Seventy, twenty, ten. The proportions are the input, and&lt;br&gt;
your code is expected to conform to them.&lt;/p&gt;

&lt;p&gt;This is the opposite. Start from what the code is, derive what the tests must be, and let&lt;br&gt;
the proportions come out wherever they come out.&lt;/p&gt;

&lt;p&gt;Here is the whole strategy up front:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Value object tests&lt;/strong&gt; -- cover one type's invariant. No isolation needed; the thing is pure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Complex leaf tests&lt;/strong&gt; -- cover one rule's decision space. No isolation needed; the thing is pure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use case tests&lt;/strong&gt; -- cover the composition: ordering, propagation, branching. Adapters stubbed, everything else real.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adapter contract tests&lt;/strong&gt; -- cover success, failure, and the translation between worlds. Run against the real dependency.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Four kinds. Nothing else. The rest of this is where each one comes from, and why the list&lt;br&gt;
is not longer or shorter.&lt;/p&gt;


&lt;h2&gt;
  
  
  The one premise
&lt;/h2&gt;

&lt;p&gt;Everything below follows from a single structural fact, and if your code does not have it,&lt;br&gt;
none of this applies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The code is a set of typed steps, composed into use cases, with errors as values and I/O&lt;br&gt;
confined to adapters.&lt;/strong&gt; A use case does not reach out to a database; it takes a step that&lt;br&gt;
does, and that step is a parameter. Failures do not throw; they travel back through the&lt;br&gt;
composition as values.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;RegisterUser&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;RegisterUser&lt;/span&gt; &lt;span class="nf"&gt;registerUser&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CheckEmailUniqueness&lt;/span&gt; &lt;span class="n"&gt;checkEmail&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                                     &lt;span class="nc"&gt;CreateValidUser&lt;/span&gt; &lt;span class="n"&gt;createUser&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                                     &lt;span class="nc"&gt;SaveUser&lt;/span&gt; &lt;span class="n"&gt;saveUser&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;// &amp;lt;- the only I/O in here&lt;/span&gt;
                                     &lt;span class="nc"&gt;GenerateToken&lt;/span&gt; &lt;span class="n"&gt;generateToken&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That shape is what makes the strategy derivable. Every decision below reads a property of&lt;br&gt;
it.&lt;/p&gt;


&lt;h2&gt;
  
  
  1. Value object tests
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The code fact:&lt;/strong&gt; a value object is pure and has zero dependencies. &lt;code&gt;Email.email(raw)&lt;/code&gt;&lt;br&gt;
takes a string and returns a &lt;code&gt;Result&amp;lt;Email&amp;gt;&lt;/code&gt;. There is nothing to stub because there is&lt;br&gt;
nothing to reach.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Therefore:&lt;/strong&gt; test it directly. Not because unit tests are virtuous, but because isolation&lt;br&gt;
is free here -- there is no assembly to do and nothing to fake.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How many tests, though?&lt;/strong&gt; That is the question people answer by habit, and habit is&lt;br&gt;
where the gaps come from. Count the decision space first, and let the count choose the&lt;br&gt;
shape:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Small and enumerable&lt;/strong&gt; -- a status enum, a three-way branch. Write examples: the space
&lt;em&gt;is&lt;/em&gt; the examples.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A finite grid&lt;/strong&gt; -- an enum against an enum, banded ranges. Write a table, one row per
cell: the cell count is known, so a missing row is a hole you can see.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unbounded&lt;/strong&gt; -- any string, any &lt;code&gt;BigDecimal&lt;/code&gt;, any timestamp. State the invariant: four
examples sample an infinite space as arbitrarily as one does.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;Quantity&lt;/code&gt; accepting 1..100 is the first kind: boundary examples genuinely cover it.&lt;br&gt;
&lt;code&gt;Email&lt;/code&gt; is the third. Four malformed strings do not cover the space of malformed&lt;br&gt;
addresses; they cover four of them. The honest test states what the parser guarantees --&lt;br&gt;
normalization is idempotent, nothing accepted violates the invariant -- which is what&lt;br&gt;
property-based testing libraries automate by generating inputs instead of listing them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not use coverage to check yourself here.&lt;/strong&gt; Four hand-picked strings reach 100% line&lt;br&gt;
coverage of &lt;code&gt;Email&lt;/code&gt;. So would two. A metric that reports the same number for a careful&lt;br&gt;
suite and a lucky one is measuring the paths the code has, not the space it decides over.&lt;/p&gt;


&lt;h2&gt;
  
  
  2. Complex leaf tests
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The code fact:&lt;/strong&gt; most business leaves are trivial -- one calculation, one transformation&lt;br&gt;
-- and are exercised whenever the use case runs. A few are not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Therefore:&lt;/strong&gt; a leaf earns its own tests when the assembled use case cannot reach its&lt;br&gt;
decision space at a price worth paying.&lt;/p&gt;

&lt;p&gt;Here is a real one. An order price calculator with three rules: a volume discount by item&lt;br&gt;
count, an order-value discount, a shipping rule that depends on the discounted total. Plus&lt;br&gt;
the rule that the two discounts do not stack -- the customer gets the better of them.&lt;/p&gt;

&lt;p&gt;Three volume bands, two value bands, three shipping outcomes: &lt;strong&gt;eighteen combinations.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Two things fall out of that number, and both arrive before any test is written. First,&lt;br&gt;
three of the eighteen are &lt;em&gt;impossible&lt;/em&gt;: an order over the large-order threshold, discounted&lt;br&gt;
by at most 10%, always clears free shipping, so a large order can never pay flat shipping.&lt;br&gt;
That is a fact about the rules, and counting is how you find it. Second, the fifteen that&lt;br&gt;
remain want a table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@ParameterizedTest&lt;/span&gt;
&lt;span class="nd"&gt;@CsvSource&lt;/span&gt;&lt;span class="o"&gt;({&lt;/span&gt;
    &lt;span class="c1"&gt;// unit,  items, oversized, expected   -- subtotal / volume / value / shipping&lt;/span&gt;
    &lt;span class="s"&gt;"  20.00,     5, false,      109.99"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;//   100 / none / low  / flat&lt;/span&gt;
    &lt;span class="s"&gt;"   4.00,    25, false,      104.99"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;//   100 / 5%   / low  / flat&lt;/span&gt;
    &lt;span class="s"&gt;"   2.00,    50, false,       99.99"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;//   100 / 10%  / low  / flat&lt;/span&gt;
    &lt;span class="c1"&gt;// ... twelve more&lt;/span&gt;
    &lt;span class="s"&gt;"  40.00,    25, false,      950.00"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;//  1000 / 5% vs 5%: equal, so no stacking&lt;/span&gt;
    &lt;span class="s"&gt;"  20.00,    50, false,      900.00"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;//  1000 / 10% beats 5%: volume wins&lt;/span&gt;
&lt;span class="o"&gt;})&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;calculate_computesTotal_acrossTheDecisionSpace&lt;/span&gt;&lt;span class="o"&gt;(...)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now compare that with testing the same fifteen through the use case. Each vector there&lt;br&gt;
needs six stubs, a request with lines and an address, and an &lt;code&gt;await()&lt;/code&gt; -- roughly fourteen&lt;br&gt;
lines against one row. Two hundred lines of assembly to check a multiplication, and every&lt;br&gt;
failure reports "PlaceOrder failed" rather than naming the rule that broke.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The common heuristic -- three or more branches, write dedicated tests -- works.&lt;/strong&gt; Keep&lt;br&gt;
it. Just know that it cannot see a decision expressed as &lt;em&gt;data&lt;/em&gt;: a limit resolved by&lt;br&gt;
looking up two enumerations, seven types against five tiers, is thirty-five answers wrapped&lt;br&gt;
in four conditionals, and rates borderline on a branch count while being the most&lt;br&gt;
combinatorial rule in the system.&lt;/p&gt;


&lt;h2&gt;
  
  
  3. Use case tests
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The code fact:&lt;/strong&gt; the use case is a composition. Its steps are parameters. The only thing&lt;br&gt;
on the path that cannot run in a test is I/O, and I/O lives at adapters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Therefore:&lt;/strong&gt; assemble the whole use case, stub only the adapters, and test the behavior.&lt;br&gt;
The stub set is not a preference -- it is the minimum that makes the test runnable, and&lt;br&gt;
anyone with the same code would compute the same set.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;execute_fails_whenEmailAlreadyExists&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;CheckEmailUniqueness&lt;/span&gt; &lt;span class="n"&gt;checkEmail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;EMAIL_ALREADY_REGISTERED&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;promise&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="c1"&gt;// remaining steps stubbed for success&lt;/span&gt;

    &lt;span class="n"&gt;useCase&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;execute&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;validRequest&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
           &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;await&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
           &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onSuccess&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;Assertions:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;fail&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is where behavior lives, so this is where most of the interesting tests are: happy&lt;br&gt;
path, each step's failure, each branch condition, error propagation through the chain.&lt;/p&gt;

&lt;p&gt;Two rules make this level work, and both are easy to get wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Assert on the outcome, except where the effect is invisible in it.&lt;/strong&gt; A transfer that&lt;br&gt;
retried twice looks identical to one that never retried. A transfer that wrote an audit&lt;br&gt;
entry looks identical to one that dropped it. There, capturing the call is the only oracle&lt;br&gt;
that can see the behavior:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;execute_auditsSuccessfulTransfer&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;audited&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ArrayList&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AuditEntry&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;();&lt;/span&gt;
    &lt;span class="nc"&gt;AuditLog&lt;/span&gt; &lt;span class="n"&gt;auditLog&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="n"&gt;audited&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unit&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt; &lt;span class="o"&gt;};&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="n"&gt;assertEquals&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;audited&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everywhere else, capturing calls couples the test to the implementation for nothing. The&lt;br&gt;
rule is neither "avoid mocks" nor "verify interactions": &lt;strong&gt;assert on the effect where the&lt;br&gt;
effect is visible, and on the call only where it is not.&lt;/strong&gt; That produces far fewer&lt;br&gt;
interaction assertions than a mock-first habit, and a firmly non-zero number, which a&lt;br&gt;
no-mocks rule gets wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One composition test for propagation, N cheap vectors for the space.&lt;/strong&gt; When validation&lt;br&gt;
lives in its own type, test it there -- that is where the vectors are cheap -- and keep&lt;br&gt;
exactly one use case test proving that a validation failure short-circuits the remaining&lt;br&gt;
steps. That fact is a property of the chain and exists nowhere else. Everything beyond it&lt;br&gt;
is duplication that breaks twice on every change.&lt;/p&gt;


&lt;h2&gt;
  
  
  4. Adapter contract tests
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The code fact:&lt;/strong&gt; an adapter is the one place two worlds meet. It converts a domain call&lt;br&gt;
into SQL or HTTP, and converts what comes back -- including exceptions -- into typed&lt;br&gt;
domain values.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Therefore:&lt;/strong&gt; it needs tests against the real dependency, and it needs exactly three&lt;br&gt;
kinds: it succeeds, it fails the way the domain expects, and it translates a transport&lt;br&gt;
exception into a typed failure.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;findById_wrapsException_whenDatabaseFails&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;JooqUserRepository&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;failingConnection&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SQLException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"reset"&lt;/span&gt;&lt;span class="o"&gt;)));&lt;/span&gt;

    &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;SOME_ID&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;await&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onSuccess&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;Assertions:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;fail&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onFailure&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cause&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;assertInstanceOf&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;UserError&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;StorageUnavailable&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cause&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;This is the layer most often missing, and skipping it undermines everything above it.&lt;/strong&gt;&lt;br&gt;
Every stub in every use case test is an assumption about a boundary: that the real adapter&lt;br&gt;
succeeds that way, fails that way, translates that way. Nothing in the use case tests&lt;br&gt;
checks any of it. The contract test is what makes the stub honest, and a suite of&lt;br&gt;
integration-first tests without contract tests is a suite whose stubs nobody has verified.&lt;/p&gt;

&lt;p&gt;The third case is the one that matters most and the one usually absent. Translation is the&lt;br&gt;
adapter's entire job, and no use case test can reach it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this is better than choosing a ratio
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Every test can name why it exists.&lt;/strong&gt; Not "we needed coverage" -- a property of the code&lt;br&gt;
put it there. That makes tests reviewable on a basis other than taste, and it makes&lt;br&gt;
deleting one a decision you can argue about with evidence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The proportions become an output.&lt;/strong&gt; You never argue about whether you have enough unit&lt;br&gt;
tests. You have as many as the pure-and-combinatorial parts warrant, as many use case tests&lt;br&gt;
as the compositions warrant, and if that comes out diamond-shaped rather than pyramid&lt;br&gt;
shaped, that is information about your code, not a violation of a rule.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nothing is tested twice.&lt;/strong&gt; Each fact is tested where it lives: invariants in the type,&lt;br&gt;
decision spaces in the rule, propagation in the composition, translation in the adapter.&lt;br&gt;
Duplication is what makes suites brittle, and duplication is what a shape-first strategy&lt;br&gt;
produces, because the shape does not know what is already covered.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Refactoring stops breaking tests.&lt;/strong&gt; Tests assert on outcomes, and stubs sit at boundaries&lt;br&gt;
that only move when the architecture moves. Rename an internal method and nothing turns&lt;br&gt;
red.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gaps become visible.&lt;/strong&gt; A table with a known cell count shows its holes. A decision count&lt;br&gt;
gives you a target before you start. Neither line coverage nor a branch count can do that,&lt;br&gt;
because both measure the shape of the code rather than the size of what it decides.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this needs from you, and what it does not
&lt;/h2&gt;

&lt;p&gt;It needs the premise. If your business logic reaches out to a database directly, the stub&lt;br&gt;
set is not "the adapters" and the derivation does not run. Fixing that is a design change,&lt;br&gt;
not a testing change -- which is the honest answer, and also the useful one, since a branch&lt;br&gt;
you cannot reach in a test is usually telling you about a hidden dependency rather than&lt;br&gt;
about your test suite.&lt;/p&gt;

&lt;p&gt;It does not need a new framework. Everything here is JUnit and parameterized tests, apart&lt;br&gt;
from the one place property-based testing earns its keep.&lt;/p&gt;

&lt;p&gt;And it does not need the pyramid. Count what your code decides, put each test where the&lt;br&gt;
fact it checks actually lives, and let the shape fall out.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The structure assumed here -- typed steps, errors as values, use cases composed from small&lt;br&gt;
pieces, I/O confined to adapters -- is described in&lt;br&gt;
&lt;a href="https://leanpub.com/jbct-book" rel="noopener noreferrer"&gt;Java Backend Coding Technology&lt;/a&gt;, along with the worked&lt;br&gt;
examples the tests above are drawn from.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>java</category>
      <category>softwaredevelopment</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Software's Second Free Lunch</title>
      <dc:creator>Sergiy Yevtushenko</dc:creator>
      <pubDate>Wed, 22 Jul 2026 12:42:59 +0000</pubDate>
      <link>https://dev.to/siy/softwares-second-free-lunch-10fk</link>
      <guid>https://dev.to/siy/softwares-second-free-lunch-10fk</guid>
      <description>&lt;h1&gt;
  
  
  Software's Second Free Lunch
&lt;/h1&gt;

&lt;p&gt;Two decades ago Herb Sutter wrote &lt;a href="http://www.gotw.ca/publications/concurrency-ddj.htm" rel="noopener noreferrer"&gt;"The Free Lunch Is Over"&lt;/a&gt;. The lunch was hardware: for years you could write ordinary single-threaded code and wait for next year's faster chip to speed it up, for free. Then clock speeds stalled, and the bill arrived as an instruction. If you want more performance now, you have to parallelize your code.&lt;/p&gt;

&lt;p&gt;There is a second free lunch, and it has been running much longer. It is the belief that you can scale software development by adding developers. It is ending the same way Sutter's did, for a structurally identical reason, and the bill reads almost the same.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem is scaling
&lt;/h2&gt;

&lt;p&gt;Strip the topic down and it is one word: scaling. Not the software's runtime scaling, the familiar kind, but the scaling of the act of building it. You would like output to grow with the size of the team, and it does not. Fred Brooks named the failure fifty years ago: adding people to a late project makes it later. The cause was never idleness. It was communication. Put n people on one tangled task and the pairs who must coordinate grow as n(n-1)/2, so past a small team most of the added labor goes to keeping everyone consistent rather than producing anything.&lt;/p&gt;

&lt;p&gt;The reflex answer today is not "add people." It is "add AI." And it is worth being blunt about this, because AI is sold as the thing that finally makes development scale, and it does not. AI attacks the cost of a single worker. Each agent writes code faster and cheaper. It does nothing to the coupling that caps how many workers can help at once. Point a swarm of agents at a tangled system and you get Brooks amplified: merge conflicts, agents holding inconsistent pictures of the same design, the quadratic coordination surface fully intact and now without even the tacit human coordination that used to soften it. The fundamental limiter stays exactly where it was.&lt;/p&gt;

&lt;p&gt;So both easy answers fail for the same reason, and naming it is the whole point of what follows. Development does not scale because the work is coupled. Until that changes, more workers, human or synthetic, buy less and less.&lt;/p&gt;

&lt;h2&gt;
  
  
  We have tried to scale, and we know how it goes
&lt;/h2&gt;

&lt;p&gt;Every serious attempt to scale development is, underneath, a way of partitioning the work so that pieces can proceed at once. Microservices cut the system into services and give each to a team. Vertical slices cut by feature. Domain-driven design cuts by bounded context. Offshoring cuts by handing whole areas to another building. They differ in the cut; they agree in the strategy.&lt;/p&gt;

&lt;p&gt;They also tend to stall in the same place, and it is instructive. They partition by the wrong key. Cut by noun, by domain object, by a service boundary drawn on a whiteboard, and the pieces you get still change together, because the thing that actually forces code to change was not what you cut along. So the parts stay coupled across the new boundaries, the coordination you tried to remove reappears as cross-service choreography and shared databases and integration meetings, and Brooks collects his tax through the back door. The partition was real; the independence was not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Borrow the trick that actually scales
&lt;/h2&gt;

&lt;p&gt;There is a kind of computing that scales almost perfectly, and the reason is worth stealing. A GPU gets its throughput by demanding two things of the work: that the pieces be &lt;strong&gt;uniform&lt;/strong&gt;, the same operation shape repeated, and that they be &lt;strong&gt;independent&lt;/strong&gt;, needing little or no communication with each other. Give it work with those two properties and it runs thousands of pieces at once. Give it tangled or divergent work and it crawls. The hardware did not invent parallelism; it insisted the &lt;em&gt;work&lt;/em&gt; be shaped for it.&lt;/p&gt;

&lt;p&gt;Shape development the same way and it parallelizes the same way. Split the design into units that are as uniform and as independent as you can make them, and you can run them at once across as many builders as you have, human or agent, because the builders barely need to talk. That is not a new wish. Shared-nothing architectures, embarrassingly parallel workloads, and vertical slices are all reaching for it. What has been missing is a reliable way to actually get the two properties, rather than draw a boundary and hope.&lt;/p&gt;

&lt;p&gt;Independence comes from cutting along the right key. Attribute each use case to the change driver that moves it, group the ones that share a driver, and let groups meet only through stated contracts and a few named coordination points. Now two units that change for different reasons have no shared code to collide in, and the coordination that generated Brooks' quadratic simply has nothing to attach to.&lt;/p&gt;

&lt;p&gt;Uniformity comes from building every unit out of the same small structural vocabulary, so any builder can pick up any unit and already know its shape. This is where a disciplined implementation earns its keep: when every unit looks alike, onboarding to a unit costs almost nothing, and a worker is fungible across the whole system rather than trapped in the one subsystem they happened to learn.&lt;/p&gt;

&lt;p&gt;And it goes deeper than the use case, which is the part that surprised me. Inside a single use case the steps share only their typed input and output. That makes them independently buildable even when they are not independently &lt;em&gt;runnable&lt;/em&gt;: a chain that executes strictly in order, A then B then C, still has three pieces that three people can build separately, because each needs to know only its own input and output type and the composition wires them together. Development parallelism is not runtime parallelism. So the grain is adjustable, use case or step, and the finer you cut the more parallel you go. The cut is not free, defining an intermediate type is its own small tax, so there is an optimal grain the way a GPU has a thread-launch overhead. A uniform implementation is what keeps that per-cut tax down to little more than a type signature.&lt;/p&gt;

&lt;p&gt;The payoff is the list you would expect from the GPU: near-zero onboarding, workers you can reassign for free because the units are interchangeable, a team you can grow without the coordination explosion. And notice who the ideal worker now is. A cheap, fungible, zero-onboarding worker that executes a well-specified unit and needs no water-cooler context is exactly what an AI agent is. AI was never the parallelization strategy. It is the workforce that strategy unlocks, and it is why doing this is worth more now than it has ever been.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bottleneck this uncovers
&lt;/h2&gt;

&lt;p&gt;Push development parallelism to its limit and the ceiling does not disappear; it moves. When coding no longer bottlenecks, the thing that does is the act of deciding the decomposition itself. And here a second quadratic lies in wait. Judged the usual way, deciding whether a set of use cases is cohesive is a pairwise question asked of every pair, which grows with the square of their number and is re-argued every time the system does. Solve coding-scaling and you would simply hit design-scaling instead, having moved the wall rather than removed it.&lt;/p&gt;

&lt;p&gt;The same key that bought independence pays this debt too. Because each use case is attributed to its own driver rather than weighed against every other, grouping becomes a labeling pass and a sort instead of an all-pairs comparison, and the cost of keeping a decomposition coherent grows quasi-linearly with the number of use cases rather than quadratically. Design scales. Both layers scale now, and what is left strictly serial is only the derivation, the act of deciding the cuts and the contracts. Set those, and everything below is parallel all the way down.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does this contradict Brooks? No.
&lt;/h2&gt;

&lt;p&gt;It confirms him. Brooks' law is dominated by two costs, ramp-up and communication. Uniformity drives ramp-up toward zero; independence drives communication toward zero. Brooks even named the escape himself: people and months become interchangeable, he wrote, when a task can be partitioned among workers with "no communication among them." That is not a loophole in the law. It is a condition the law states outright, and the method is built to meet it on purpose.&lt;/p&gt;

&lt;p&gt;What governs the work once you have stepped out of Brooks' regime is a different law, Amdahl's. The speedup is bounded by the fraction that stays serial, and that fraction is now just the derivation plus the shared kernel and the final integration. Because the derivation itself was made quasi-linear, that serial fraction is small, and a small serial fraction is exactly what a high ceiling looks like. Less Brooks, more Amdahl, and that trade is the entire reason it scales.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lunch
&lt;/h2&gt;

&lt;p&gt;The second free lunch is not that development became free. It is that the workforce did. A cheap, endlessly available, zero-onboarding workforce now exists, and the only thing standing between it and a system is whether the work has been shaped into pieces it can take independently. That shaping is the price of the lunch, and it is a design problem, not a model problem.&lt;/p&gt;

&lt;p&gt;Sutter's bill said: parallelize your code. This one says: parallelize your development. It turns out to be the same recipe, uniform and independent pieces run in parallel, and it was always going to be, because the constraint was never really about silicon or about people. It was about coupling.&lt;/p&gt;

&lt;p&gt;The method, and the worked derivations behind it, are at pragmatica.dev.&lt;/p&gt;

</description>
      <category>softwareengineering</category>
      <category>architecture</category>
      <category>softwaredesign</category>
      <category>programming</category>
    </item>
    <item>
      <title>Less Art, More Engineering: It Wasn't Measurement</title>
      <dc:creator>Sergiy Yevtushenko</dc:creator>
      <pubDate>Tue, 21 Jul 2026 11:35:43 +0000</pubDate>
      <link>https://dev.to/siy/less-art-more-engineering-it-wasnt-measurement-2916</link>
      <guid>https://dev.to/siy/less-art-more-engineering-it-wasnt-measurement-2916</guid>
      <description>&lt;h1&gt;
  
  
  Less Art, More Engineering: It Wasn't Measurement
&lt;/h1&gt;

&lt;p&gt;Two and a half years ago I wrote an article called &lt;a href="https://medium.com/@sergiy-yevtushenko/less-art-more-engineering-c01582feb7cb" rel="noopener noreferrer"&gt;"Less Art, More Engineering"&lt;/a&gt;. The argument was simple: software development calls itself engineering but mostly isn't, because it runs on subjective criteria with no verifiable definitions. Readability with no metric. Best practices that contradict each other. Design methods that offer no way to check whether a design is correct. I ended on a promise: the next step is to convert what we know into a set of clearly defined, well grounded rules and criteria. "Ideally measurable," I wrote, "but in either case easily verifiable."&lt;/p&gt;

&lt;p&gt;I want to report back on that promise, because I got the diagnosis right and the cure wrong.&lt;/p&gt;

&lt;p&gt;None of this began as a plan. The original goal was modest: organize code so its structure would be recognizable and its behavior predictable. I had nothing in mind beyond that. But the moment I asked what actually makes structure recognizable, the question refused to stay small. Answering it meant understanding low-level structure and the forces that shape it, and each answer uncovered the next one. That inquiry grew into Process-First Design, and Process-First Design in turn produced Architecture Synthesis. I set out to tidy functions and ended up deriving systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I tried
&lt;/h2&gt;

&lt;p&gt;In 2025 I made the first real attempt, in an article called &lt;a href="https://medium.com/codex/beyond-best-practices-e36511c073e2" rel="noopener noreferrer"&gt;"Beyond Best Practices"&lt;/a&gt;. Instead of collecting an endless pile of best practices, I proposed five criteria to assess any coding decision: mental overhead, business/technical ratio, design impact, reliability, and complexity. Five stable criteria in place of countless ad-hoc rules. They work. You can hold two approaches against them and get a defensible comparison, and I still use them.&lt;/p&gt;

&lt;p&gt;But look at what happened to the word "measurable" between the two articles. In 2023 I wanted measurement. By 2025 I was already writing that the criteria "could be either somehow measured or, at least, checked for presence/absence," and that "absolute numbers might not be that useful per se" as long as they "enable objective comparison." I had quietly retreated from measuring to comparing, and I had not noticed. That erosion, measurable to comparable to merely present-or-absent, was the whole story trying to surface. I just wasn't listening to it yet.&lt;/p&gt;

&lt;p&gt;There was a second thing I glossed over. I called them "my own set of criteria." I chose those five, and I chose that they matter. The subjectivity I set out to remove had not left. It had gotten smaller and moved somewhere I could see it, but it was still mine.&lt;/p&gt;

&lt;h2&gt;
  
  
  What surprised me
&lt;/h2&gt;

&lt;p&gt;Here is the part I did not expect. The thing that actually removed subjective technical decisions was not measurement at all. It was determinism.&lt;/p&gt;

&lt;p&gt;A measurement gives you a number. A derivation gives you an answer that two people, starting from the same inputs and following the same rules, both reach, without either of them exercising taste. That second property is what I had wanted the whole time. I had assumed the only road to it was to measure things, so I chased metrics for two years. But you do not need to measure anything to be objective in the way that matters. You need the decision to be derived by a fixed procedure rather than chosen by judgment. A derivation is reproducible even when nothing on the page is a number.&lt;/p&gt;

&lt;p&gt;I had been treating "objective" and "measured" as the same word. Under contact with real work they came apart, and the one that did the job was not the one I had bet on.&lt;/p&gt;

&lt;p&gt;The clearest case is architecture. I built a &lt;a href="https://pragmatica.dev/method/architecture-synthesis/" rel="noopener noreferrer"&gt;procedure&lt;/a&gt; that takes a system's service-level objectives (latency, availability, consistency, load) and derives an architecture from them through a fixed set of rules: which parts deploy together, where data lives, where consistency holds, how failure is handled. Nobody argues about the result. You state the requirements, you apply the rules, you get an answer. Run it again tomorrow and you get the same answer. No measurement of "goodness" appears anywhere in it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the judgment went
&lt;/h2&gt;

&lt;p&gt;None of this eliminates judgment. It concentrates it, and that turned out to be the actual achievement.&lt;/p&gt;

&lt;p&gt;It concentrates in two places, honestly and in the open. The first is the criteria and the rules themselves: someone has to decide what the procedure optimizes for, and that is a judgment, made once and visibly, instead of a thousand times in the dark. The second is the business inputs. A derivation is only as good as the requirements it starts from, and those come from the business.&lt;/p&gt;

&lt;p&gt;This shows up most sharply in failure. The technology does not prevent failure. What it does is change where failure comes from. A derived system that fails, fails because a business decision was wrong: a service-level objective that did not reflect reality, a requirement nobody stated, a constraint priced too low. The subjective technical decision, the place we used to lose systems to taste and seniority and the loudest voice in the review, is mostly gone. What is left is business truth, and business truth is the right thing to be arguing about.&lt;/p&gt;

&lt;p&gt;I did not confirm any of this by measuring it. I confirmed it by deriving four systems I do not control, Stack Overflow, Shopify, Discord, and a national companies registrar, from their public commitments alone, registering the predictions before checking them against reality, and grading the result in the open. It gets things right and it gets things wrong. It predicted read replicas for Stack Overflow, where the real answer is a mostly memory-resident database. The miss is kept, because a method that never fails is a parlor trick. That, and not any metric, is what "verifiable" finally meant: a procedure whose output you can check, and whose failures you can point at.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I did not see coming
&lt;/h2&gt;

&lt;p&gt;In November 2023 I was not thinking about AI. I should have been.&lt;/p&gt;

&lt;p&gt;That article had an economic argument buried in it. Subjectivity does not scale, I wrote, because every new developer has to grow their own, unlike a method you can teach quickly. It was a headcount argument. What I did not see is that a procedure deterministic enough to teach a person quickly is also a procedure a machine can execute.&lt;/p&gt;

&lt;p&gt;This is not a hopeful guess. When I graded those four derivations, the operators that ran them were AI agents following the written rules, with no way to browse to the answer. A machine reaching the documented architecture from the requirements and the rulebook alone is the strongest evidence I have that the method has left the realm of taste. The property I wanted for human reasons, that the method be a formula and not a feeling, is the same property that lets a machine apply it. The free lunch I predicted would end is ending, and AI is why the deterministic alternative is now the one that wins.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this leaves the promise
&lt;/h2&gt;

&lt;p&gt;I was right that software runs on too much subjectivity and that this cannot last. I was wrong that measurement was the way out. The way out is determinism: fixed procedures, deterministically applied, that concentrate the judgment we cannot remove into the two places it belongs, the choice of what matters and the truth of the business, and derive everything else.&lt;/p&gt;

&lt;p&gt;That is less than I promised in 2023 and more than I expected in 2025. There is still subjectivity in it, at the criteria and at the inputs, and I would rather say so than pretend otherwise. If you want to test it, the honest way is not to read more about it. It is to take the procedure, run it on a system you know, and try to make it produce an answer you know is wrong. That is the most useful thing that could happen to it.&lt;/p&gt;

&lt;p&gt;The procedure, the worksheet, and the four graded derivations, misses included, are at pragmatica.dev.&lt;/p&gt;

</description>
      <category>softwareengineering</category>
      <category>architecture</category>
      <category>softwaredesign</category>
      <category>programming</category>
    </item>
    <item>
      <title>How Architecture Emerges</title>
      <dc:creator>Sergiy Yevtushenko</dc:creator>
      <pubDate>Mon, 06 Jul 2026 13:25:51 +0000</pubDate>
      <link>https://dev.to/siy/how-architecture-emerges-1b9</link>
      <guid>https://dev.to/siy/how-architecture-emerges-1b9</guid>
      <description>&lt;p&gt;&lt;em&gt;Never debate architecture again&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Two teams build the same product: software that sells a ticket to a seat at an event. One team ships a single program talking to one database. The other ships a multi-region platform of subsystems trading events across a bus. They share almost nothing.&lt;/p&gt;

&lt;p&gt;Neither team made a mistake.&lt;/p&gt;

&lt;p&gt;If two architectures that share nothing can both be correct for the same domain, then the domain never determined the architecture. Something else did. In most teams, that something else is taste: the last conference talk, the article about how a big platform does it, the loudest voice in the review. Architecture selection is the least standardized decision in backend engineering, and we've normalized that by calling it "trade-off analysis."&lt;/p&gt;

&lt;p&gt;There's a better answer: the two teams had different &lt;strong&gt;answers to the same questions&lt;/strong&gt;, and the architectures follow from the answers. Mechanically. Both teams ran the same derivation; the inputs differed, so the outputs did. Architecture is not a choice you defend. It's an output you compute, and recompute when the inputs change.&lt;/p&gt;

&lt;p&gt;This article runs that derivation, end to end, on the smaller of the two teams. It fits in a few hundred words, and every step cites the input that forced it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Catalogs, physics, and the missing procedure
&lt;/h2&gt;

&lt;p&gt;Almost everything written about software architecture is either a &lt;strong&gt;catalog&lt;/strong&gt; (styles, patterns, trade-off matrices: pick one, defend your pick) or &lt;strong&gt;physics&lt;/strong&gt; (how replication, queues, and consensus actually behave). Both are useful. Neither tells you what to &lt;em&gt;do&lt;/em&gt; on Monday: catalogs give you options without a selection procedure, physics gives you mechanisms without a decision path.&lt;/p&gt;

&lt;p&gt;What's missing is a &lt;strong&gt;procedure&lt;/strong&gt;: elicit these inputs, run this derivation, get an architecture out, re-derive when the inputs move. Hardware engineering has worked this way for decades; behavioral spec plus constraints in, circuit out. Software still picks styles from articles.&lt;/p&gt;

&lt;h2&gt;
  
  
  The inputs: eleven questions
&lt;/h2&gt;

&lt;p&gt;Before any architecture is on the table, ask what the system must satisfy. Eleven questions cover enterprise backend work. The answers vary wildly between a single venue and a global platform; the questions do not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Service-level objectives&lt;/strong&gt; (per use case or data class):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Latency, per use case — usually P95/P99&lt;/li&gt;
&lt;li&gt;Throughput — steady-state and peak&lt;/li&gt;
&lt;li&gt;Availability, per use-case criticality&lt;/li&gt;
&lt;li&gt;Consistency contract, per data class — strict / read-your-writes / bounded-staleness / eventual&lt;/li&gt;
&lt;li&gt;Durability — the recovery-point objective&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Constraints:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Compliance — regulatory and contractual regimes&lt;/li&gt;
&lt;li&gt;Technology and platform mandates&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Operational targets:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Deploy frequency and safety&lt;/li&gt;
&lt;li&gt;Cost shape — a ceiling or a per-operation budget&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Substrate-shaping forces:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Multi-X — multi-country, -currency, -tenant, -region&lt;/li&gt;
&lt;li&gt;Scale shape — read-heavy, write-heavy, event-heavy, mixed&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One discipline before you answer: &lt;strong&gt;an answer only counts once its cost is acknowledged.&lt;/strong&gt; "We need 99.99%" is not an answer until someone has seen what a fourth nine costs and still wants it. An SLO you haven't priced is a wish.&lt;/p&gt;

&lt;h2&gt;
  
  
  The output: six axes
&lt;/h2&gt;

&lt;p&gt;An architecture is not a named style. It's a position on six independent axes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Axis&lt;/th&gt;
&lt;th&gt;Values&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Deployment topology&lt;/td&gt;
&lt;td&gt;single deployable / multiple / unified runtime / serverless&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Composition substrate&lt;/td&gt;
&lt;td&gt;direct calls / event-based / streaming&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read/write model&lt;/td&gt;
&lt;td&gt;unified / separated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;State storage&lt;/td&gt;
&lt;td&gt;current-state / event-sourced&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Persistence&lt;/td&gt;
&lt;td&gt;single shared / distributed / sharded / per-component / polyglot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recovery&lt;/td&gt;
&lt;td&gt;compensate-by-inverse / degrade-and-continue / design-the-failure-out&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;"Microservices vs monolith" is one value on one axis. The other five axes exist whether or not you name them. Most preference wars are two people arguing about different axes without noticing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The derivation: three rules
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Start at the cheapest position on every axis&lt;/strong&gt; — single deployable, direct calls, unified reads, current-state, one shared store.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Move an axis only when an answer is not contained by the current position.&lt;/strong&gt; An answer the cheap position already satisfies presses on nothing; it's inert.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When you must move, take the cheapest value that contains the demand, at the narrowest scope that contains it.&lt;/strong&gt; Event-source one data class, not the system. Split one read path, not the world.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;(There's a fourth rule for when two answers press one axis in opposite directions: the system splits at the boundary between them. The small team below never needs it. That's the point.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Running it: the independent venue
&lt;/h2&gt;

&lt;p&gt;One venue runs its own box office. One team, one region. The answer sheet:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Buying a ticket: ~2s at P95. Availability check: under 1s.&lt;/li&gt;
&lt;li&gt;Bookings: tens per second on a busy night. Checks: a bit higher.&lt;/li&gt;
&lt;li&gt;Service availability: 99.5% — hours of downtime a year is survivable for one venue.&lt;/li&gt;
&lt;li&gt;Booking data: strictly consistent, zero data loss on confirmed sales. Availability and price reads: eventual is fine.&lt;/li&gt;
&lt;li&gt;One country, one currency. Daily deploys. Tight cost ceiling. Read-moderate load.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now walk the axes and watch what presses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deployment.&lt;/strong&gt; One team, bounded ops budget, one region. Nothing demands independent releases, independent scaling, or isolated failure domains. Nothing presses. &lt;strong&gt;Single deployable.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Substrate.&lt;/strong&gt; Event-based composition earns its cost across deployment boundaries, and there are none to cross. A queue would add propagation lag and delivery semantics to paths that a function call serves. Nothing presses. &lt;strong&gt;Direct calls.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read/write model.&lt;/strong&gt; Reads and writes have the same shape and no separate read SLO exists. A projection would be machinery against a problem nobody stated. &lt;strong&gt;Unified.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Storage.&lt;/strong&gt; No answer demands replay or historical reconstruction. &lt;strong&gt;Current-state.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Persistence.&lt;/strong&gt; 2 seconds at P95, tens per second, strict booking consistency: a single store contains all of it, and gives strict consistency away for free via ordinary transactions. &lt;strong&gt;Single shared store.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recovery.&lt;/strong&gt; The only axis the domain forces. Booking moves money through sequenced steps: reserve the seat, authorize payment, confirm. Every step has a defined inverse: release, void. &lt;strong&gt;Compensate by inverse.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The vector: &lt;em&gt;single deployable / direct / unified / current-state / single shared store / compensation.&lt;/em&gt; A boring program and one database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The team didn't choose simplicity. The derivation found zero uncontained demands.&lt;/strong&gt; Every answer sat comfortably inside the cheapest position, so every axis stayed put. That's not minimalism as virtue. It's arithmetic. The same arithmetic cuts the other way: copying a big platform's architecture into this answer sheet means paying, forever, for capabilities no answer demands. That's how small platforms drown.&lt;/p&gt;

&lt;p&gt;If you want to see the same derivation produce the multi-region event platform, change the answers: a contractual 200ms P99 on the quote path, on-sale bursts in the hundreds of thousands per minute, regulators demanding replayable price history, data-sovereignty rules. Different inputs, same procedure: each new demand breaks containment somewhere specific, and only that axis moves, at only that scope.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the answers change
&lt;/h2&gt;

&lt;p&gt;The venue grows. It joins a regional network; a contract now specifies sub-500ms availability checks; reads go heavy. What happens to the architecture?&lt;/p&gt;

&lt;p&gt;One thing: the read path, and only the read path, has an answer the current position no longer contains. The derivation moves one axis at one scope: read replicas behind the same unified model (cheaper than projections, and the staleness budget allows it). Everything else stays. No migration program, no target state, no two-year roadmap. &lt;strong&gt;The next correct step, computed from the new answers.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's the reframe: an architecture is not a state your system is in. It's a &lt;strong&gt;derivative&lt;/strong&gt; — the next correct step from where the system stands, given the forces on it now. Greenfield is just the derivative taken from a blank page. The system you inherited, carrying decisions nobody alive remembers making, is the same computation from a harder starting point.&lt;/p&gt;

&lt;p&gt;The reframe also works backward, as an audit. For every axis position your current system holds, name the answer that forces it. Any position no answer forces is technical debt: not by opinion, by construction. You're paying for a capability nobody asked for. Run it on three microservices that share a database and deploy in lockstep, and the test flags the deployment split in about thirty seconds: no live answer demands it, and the fix is a merge, not another split.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this comes from
&lt;/h2&gt;

&lt;p&gt;The derivation above is the Architecture Synthesis method from &lt;a href="https://leanpub.com/process-first-design" rel="noopener noreferrer"&gt;Process-First Design&lt;/a&gt;, a methodology for deriving backend systems from business processes, of which architecture is the last derivation step. The condensed edition is free and reads in about thirty minutes. The procedure has been validated by re-deriving every published worked example (three ticketing profiles at three scales, plus a payroll platform someone else built) from their answer sheets alone. The derivations where the answers turn hostile and hybrids start appearing on purpose (a regional platform, then a multi-tenant enterprise) are worked end to end in Process-First Design, along with the brownfield case: the same procedure run on a system somebody else built.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Also in this family: &lt;a href="https://pragmatica.dev/" rel="noopener noreferrer"&gt;Java Backend Design Technology&lt;/a&gt; derives code structure from eight questions the same way this derives architecture from eleven. Same bet throughout: the decisions that never deserved judgment should be mechanical, so your judgment is spent where it's genuinely owed.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>softwaredesign</category>
      <category>programming</category>
    </item>
    <item>
      <title>Software's Industrialization Moment</title>
      <dc:creator>Sergiy Yevtushenko</dc:creator>
      <pubDate>Sat, 02 May 2026 03:33:08 +0000</pubDate>
      <link>https://dev.to/siy/softwares-industrialization-moment-4pjn</link>
      <guid>https://dev.to/siy/softwares-industrialization-moment-4pjn</guid>
      <description>&lt;p&gt;When you design a circuit, you don't invent the resistor. You don't redesign the transistor for your application. You don't write a new SPI protocol because the existing one offends you. You compose standardized components -- resistors with known tolerances, ICs with documented behavior, protocols with published semantics -- into something specific to the problem. The design is creative work. The substrate is standardized.&lt;/p&gt;

&lt;p&gt;That standardization is not an accident. Electronics went through an industrialization arc. Standard component values (E-series, mid-twentieth century). Standard fabrication processes (silicon refinement, decade by decade). Standard interfaces (Ethernet 1980, I2C 1982, USB 1996). Tooling that codified design rules so the designer doesn't re-derive them on every project. The result is a profession where the design work is dense with creative tradeoffs, but the substrate beneath those tradeoffs is industrially deep.&lt;/p&gt;

&lt;p&gt;Software has been mostly going the other direction. Each generation invents new component vocabularies, new build systems, new design patterns to internalize. The substrate underneath the creative work has not been industrialized. It has been re-invented, badly, every five years.&lt;/p&gt;

&lt;p&gt;That is changing. The arc of industrialization that manufacturing and electronics walked first has lessons software can now use -- and the steps of that arc map cleanly to specific shifts that have arrived in software over the last decade.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four moments that industrialized manufacturing
&lt;/h2&gt;

&lt;p&gt;Eli Whitney delivered a contract for ten thousand muskets in 1798 with a property that hadn't been guaranteed before: any lock fit any musket. Before Whitney, every musket lock was hand-fitted. A broken part wasn't replaced -- it was re-fitted by a craftsman who could improvise to the assembly. Whitney's parts didn't have to match the gun; they had to match a &lt;em&gt;spec&lt;/em&gt;. Variability became tolerance. Tolerance became the unit of standardization.&lt;/p&gt;

&lt;p&gt;Henry Ford's assembly line at Highland Park in 1913 took Whitney's principle further. Standardized parts let workers be standardized too. Any worker could do any station's task. The Model T didn't depend on which artisan happened to be in the shop on any given day. Quality and throughput stopped depending on tribal knowledge in the heads of senior craftsmen. The system was the technology, not any one worker's skill.&lt;/p&gt;

&lt;p&gt;The CAD/CAM revolution that began in the late 1950s and matured through the 1970s and 80s separated &lt;em&gt;what to make&lt;/em&gt; from &lt;em&gt;how to make it&lt;/em&gt;. Computer-Aided Design tools let engineers parametrize geometry rather than draw it freehand. Computer-Aided Manufacturing translated those parametric designs into machine instructions. Designer surprises -- will this fit, will this clear, will this be machinable -- collapsed because the spec was unambiguous. The design stayed creative; the path from design to fabrication became deterministic.&lt;/p&gt;

&lt;p&gt;Industrial robots arrived in 1961 with Unimate at General Motors, doing welds. The robot didn't replace the engineer. It took the part of fabrication that didn't need judgment. Engineers moved up: design, supervision, quality, problem-solving. The floor under the profession rose. The work of being an engineer changed.&lt;/p&gt;

&lt;p&gt;Each step took years to land. Each step raised the floor on what the profession could do. None of the steps replaced creative work -- they freed it from the parts that had been holding it back.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mapping the arc to software
&lt;/h2&gt;

&lt;p&gt;Each step has a software equivalent. Most of them have arrived in the last decade or two, and the arrival times -- when you line them up -- tell you where software is right now in its compressed version of the same arc.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Whitney's interchangeable parts → standardized vocabulary.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Software's standardized vocabulary in any given codebase has historically been local. This team's idioms. This framework's conventions. This language's mainstream patterns. Across teams, interchangeability dropped sharply -- a developer joining a new codebase has spent decades onboarding into local vocabulary because there was no inter-codebase standard. JBCT (Java Backend Coding Technology) commits to a small named vocabulary -- three containers, six patterns, one boundary primitive -- that's the same in any codebase that uses it. Any developer who knows the vocabulary can read any JBCT codebase. Interchangeability of parts (code structures) and of people (developers).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ford's production line → JBCT as technology.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ford's deeper contribution wasn't speed. It was that the standardization itself was the &lt;em&gt;technology&lt;/em&gt;. The assembly line was a deliberately engineered system that produced consistent output regardless of which worker staffed any given station. JBCT positions itself the same way -- not a methodology you adopt to taste, but a deliberate technology that produces consistent code regardless of which engineer wrote any given module. The system is what makes consistency possible; consistency isn't expected to flow from individual discipline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CAD/CAM → JBDT.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;JBDT (Java Backend Design Technology), the methodology articulated in &lt;em&gt;Java Backend Design Technology: A Process-First Methodology&lt;/em&gt;, is software's CAD/CAM. It separates &lt;em&gt;what the system should do&lt;/em&gt; from &lt;em&gt;how it's built&lt;/em&gt;. The designer answers a structured set of questions -- about boundaries, dependencies, modalities, and process shape -- and the answers are deterministic enough that the implementation isn't an open interpretation but a derivation. Designer surprises -- will this scale, will this be testable, will this be maintainable -- collapse because the spec is unambiguous. The design stays creative; the path from design to code becomes deterministic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Industrial robots → AI.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;AI assistants are the routine-work automation that arrived late in the manufacturing arc and is arriving now in software. They don't replace the designer. They do the parts of code production that are routine -- boilerplate, scaffolding, well-trodden compositions, test fixtures, documentation. Engineers move up: design, supervision, domain judgment, the parts of the work that benefit from human attention. The floor under the profession rises. The work of being an engineer changes.&lt;/p&gt;

&lt;p&gt;The four steps line up. JBCT is software's Ford-with-Whitney's-parts. JBDT is software's CAD/CAM. AI is software's industrial robots. The catalyst that has put all of these in conversation simultaneously is AI -- but AI is &lt;em&gt;not&lt;/em&gt; the industrialization. Large parts of JBCT existed in practice well before AI was useful -- patterns that were waiting to be named and unified. JBDT articulates the upstream design technology those patterns implied. Both reached their current articulated form with AI assistance, which is part of the point. AI didn't create the standardization; it crystallized standardization that was already there in practice. And that crystallization is what made the &lt;em&gt;next&lt;/em&gt; step -- mechanical-work automation -- immediately available on top.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this looks like in practice
&lt;/h2&gt;

&lt;p&gt;The pattern is the same across all four steps. The substrate gets standardized. The creative work concentrates on what only humans can do.&lt;/p&gt;

&lt;p&gt;In a JBCT codebase, the developer doesn't invent error-handling conventions, naming rules for types, or scaffolding for cross-cutting concerns. Those are standardized. The developer's attention goes to the domain -- what the business is trying to do, what the types should mean, where the boundaries genuinely lie.&lt;/p&gt;

&lt;p&gt;Under JBDT, the designer doesn't invent a fresh design process for each project. The eight questions, the dependency-data graph, the six patterns -- those are standardized. The designer's attention goes to the answers -- what's actually true about &lt;em&gt;this&lt;/em&gt; business, what &lt;em&gt;this&lt;/em&gt; domain genuinely requires.&lt;/p&gt;

&lt;p&gt;With AI assistance, the engineer doesn't write the boilerplate that the same engineer wrote a hundred times last year. The AI does. The engineer reviews, judges, integrates -- work that benefits from human attention because it requires judgment.&lt;/p&gt;

&lt;p&gt;The result is not less work. It is differently distributed work. The profession does not shrink -- it grows in the parts that scale with creative attention. This is what happened in manufacturing. It is what happened in electronics. It is what is happening now in software.&lt;/p&gt;

&lt;p&gt;This is the part the discourse usually misses. &lt;em&gt;Less art, more engineering&lt;/em&gt; is sometimes read as a directive to make software more rote, more constrained, less creative. The actual move is the opposite. It concentrates the creative work where creativity earns its keep, by removing the thousands of small decisions that didn't need to be creative in the first place. Naming a type. Choosing an error-handling pattern. Deciding how to thread observability through a use case. All of these are standardizable. None of them was the part of the work that drew anyone into the profession.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the analogy bends
&lt;/h2&gt;

&lt;p&gt;Software is not the same as manufacturing. The manufactured product is a physical thing whose properties don't change after it leaves the factory. Software's product is updated, extended, debugged, and reshaped continuously after delivery. The factory never closes. The bill of materials evolves. Knowledge work -- figuring out what to build, what it should mean, what edge cases lurk -- is more central in software than in cars or muskets, and that work resists the kind of standardization that reduces variability in physical parts.&lt;/p&gt;

&lt;p&gt;That is true. It is also true that &lt;em&gt;parts of&lt;/em&gt; software's production are standardizable in the same way physical parts are: the vocabulary, the patterns, the cross-cutting plumbing, the design questions. Electronics is the more honest analogy here. Electronics has the same continuous-update property in many senses -- firmware revisions, derivative designs, errata, board respins -- and electronics has industrially deep standardization despite that. The standardizable parts are the standardizable parts. The creative parts stay creative. The two layers don't compete.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's still coming
&lt;/h2&gt;

&lt;p&gt;The manufacturing arc took roughly 150 years from Whitney to mature CAD/CAM. The electronics arc took about 70, depending on where you start counting. The software arc has had about 70 years and is now hitting industrial-robots time -- compressed, but recognizable.&lt;/p&gt;

&lt;p&gt;What's still missing in the software arc is the equivalent of standardized &lt;em&gt;interfaces&lt;/em&gt; -- the USB, I2C, Ethernet of software. Standardized parts (vocabulary) and standardized processes (methodology) are the prerequisites for standardized interfaces, and the work of articulating those is now becoming possible.&lt;/p&gt;

&lt;p&gt;The book in progress, &lt;em&gt;Process-First Design&lt;/em&gt;, articulates one substrate for the next layer of standardization -- process composition vocabulary that holds across altitudes, recovery-class taxonomy, design-question stages, and how those connect. Whether that particular substrate becomes the standard or something else does is not the question. The question is what the &lt;em&gt;appearance&lt;/em&gt; of any candidate signals: that the next layer has become workable, where a decade ago it would have been premature.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;If you design a circuit today, you don't invent the resistor. You don't even think about not inventing it. The standardization is so deep that the question doesn't surface -- and the design work that does surface is dense with creative tradeoffs that the standardization made possible.&lt;/p&gt;

&lt;p&gt;Software is reaching that moment. The substrate is being standardized -- vocabulary, patterns, design questions, the parts of the work that didn't need to be invented fresh each time. The creativity concentrates where it earns its keep: domain modeling, system architecture, the judgments that benefit from human attention.&lt;/p&gt;

&lt;p&gt;Manufacturing took 150 years. Electronics took 70. Software is on compressed timelines, but the arc is recognizable. The substrate gets industrialized. The design stays creative.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article is part of work on Process-First Design -- a forthcoming book on methodology for enterprise backend software. Earlier pieces in the series, including &lt;a href="https://medium.com/@sergiy-yevtushenko/saga-is-not-a-pattern-6973bdcebde5" rel="noopener noreferrer"&gt;&lt;em&gt;Saga Is Not a Pattern&lt;/em&gt;&lt;/a&gt;, develop specific implications of the principle described above.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>softwaredesign</category>
      <category>methodology</category>
      <category>programming</category>
    </item>
    <item>
      <title>When Types Become the Business Language</title>
      <dc:creator>Sergiy Yevtushenko</dc:creator>
      <pubDate>Wed, 22 Apr 2026 20:27:00 +0000</pubDate>
      <link>https://dev.to/siy/when-types-become-the-business-language-1am0</link>
      <guid>https://dev.to/siy/when-types-become-the-business-language-1am0</guid>
      <description>&lt;p&gt;Software has a language problem. Not "natural language vs. code" -- that one is well-understood. The harder problem is the distance between the language a domain expert speaks and the language the program uses to represent what the expert said. Every sentence the expert uses -- "the customer may not exist," "placing the order might fail," "fulfillment happens later" -- is a statement about the domain. By the time it reaches the code, it has usually been flattened into control flow, null checks, try/catch blocks, and callbacks. The domain's meaning is still there, but it's no longer speakable in the code's own vocabulary.&lt;/p&gt;

&lt;p&gt;Two traditions have tried to close this gap. Both got partway. Neither finished.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Object-Oriented Design Promised
&lt;/h3&gt;

&lt;p&gt;OO was meant to model the domain. Classes would represent domain concepts. Behavior would live with the data that behavior operates on. A programmer reading a system would see &lt;code&gt;Order&lt;/code&gt;, &lt;code&gt;Customer&lt;/code&gt;, &lt;code&gt;Invoice&lt;/code&gt; -- names pulled directly from the business -- and find business logic inside each class.&lt;/p&gt;

&lt;p&gt;That's not what enterprise OO became. Shared entity models evolved into God objects serving many features badly. DTO-to-entity-to-DTO mapping layers accumulated because no single shape fit any single feature. Architecture discussions turned into debates about aggregate boundaries. The "where does the behavior live?" question never settled -- rich model, anemic model, transaction scripts, domain services. Each project picked differently; each team mixed approaches.&lt;/p&gt;

&lt;p&gt;Entities, it turned out, are the wrong unit. A &lt;code&gt;Seat&lt;/code&gt; in a booking flow is a row and a seat number. A &lt;code&gt;Seat&lt;/code&gt; in availability checking is an occupancy flag. A &lt;code&gt;Seat&lt;/code&gt; in pricing is a fare class. These are three different processes with three different notions of what a seat even is. Forcing them to share a structural definition is how entity models grow into God objects.&lt;/p&gt;

&lt;p&gt;The business doesn't think in entities first. It thinks in processes: &lt;em&gt;place an order, confirm availability, charge a card&lt;/em&gt;. Entities appear inside processes as participants, not as durable shared structures. OO's bet -- domain modeling through shared nouns -- couldn't close the language gap because the business doesn't speak in shared nouns.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Pure Functional Programming Offered
&lt;/h3&gt;

&lt;p&gt;Functional programming took a different route. Instead of modeling the domain with classes, it modeled &lt;em&gt;computations&lt;/em&gt; with types. Every value has a type. Every effect -- failure, absence, asynchrony, state, nondeterminism -- lives in a type that says so. The type system becomes precise enough to describe not just what a value is, but what can happen while producing it.&lt;/p&gt;

&lt;p&gt;That precision is real, and for programmers writing libraries or reasoning about concurrent programs it is valuable. But it answers a different question than the one the business is asking. A signature that tracks every effect a computation performs is transparent to the programmer -- it tells them exactly what happens inside. To the domain expert, it is opaque. Each additional type parameter is another channel through which machine-level detail leaks into code that was supposed to read as business logic.&lt;/p&gt;

&lt;p&gt;Pure FP is legible to &lt;em&gt;programmers&lt;/em&gt; in a way that OO isn't. It is not legible to the &lt;em&gt;domain&lt;/em&gt; in a way that OO at least gestured at. The language gap narrowed on one axis and widened on another.&lt;/p&gt;

&lt;h3&gt;
  
  
  Two Halves of the Same Idea
&lt;/h3&gt;

&lt;p&gt;OO wanted types to carry business meaning but stopped at the nominal level: &lt;code&gt;Customer&lt;/code&gt; is a class that wraps a long and some strings. FP wanted types to carry semantic meaning but optimized for machine-level semantics: what effects, what errors, what environment.&lt;/p&gt;

&lt;p&gt;What if types carried &lt;em&gt;business&lt;/em&gt; semantics -- not just nominal ("this is a Customer") but structural ("this lookup might fail to find one")? What if the structural facts the type system expresses were the same facts the domain expert states out loud?&lt;/p&gt;

&lt;p&gt;This is the move that closes the gap. It isn't OO because behavior doesn't live with entities; it lives with processes. It isn't pure FP because the type system isn't tracking machine-level effects; it's tracking domain-level modalities. It is a middle ground that takes from each what actually serves legibility, and drops what doesn't.&lt;/p&gt;

&lt;p&gt;Java Backend Coding Technology (JBCT) is one way of writing this middle ground down.&lt;/p&gt;

&lt;h3&gt;
  
  
  Four Shapes, One Principle
&lt;/h3&gt;

&lt;p&gt;Most functions don't have a special modality. They take inputs, return a value, always succeed, return immediately. Those stay plain: return type is &lt;code&gt;T&lt;/code&gt;. No container, no ceremony. This is the baseline -- the zero-deviation case.&lt;/p&gt;

&lt;p&gt;Three containers appear only when the computation genuinely has the modality:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Option&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/strong&gt; -- the value may be missing. &lt;em&gt;"The customer may not exist."&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Result&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/strong&gt; -- the computation may fail. &lt;em&gt;"Placing the order might fail."&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Promise&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/strong&gt; -- the value may be deferred. &lt;em&gt;"Fulfillment happens later."&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Four shapes total. One principle: every deviation from a total function gets a name; no deviation needs no name. Each container in a signature is a statement the business would recognize as true about the domain. Each absence is also a statement: this computation always completes with a value.&lt;/p&gt;

&lt;p&gt;Nothing about this surface tracks machine-level detail. A function returning &lt;code&gt;Promise&amp;lt;Order&amp;gt;&lt;/code&gt; does not disclose what database it reads, what HTTP calls it makes, what logs it writes. Those are facts about &lt;em&gt;how&lt;/em&gt; the order arrives. The type describes &lt;em&gt;what the domain guarantees&lt;/em&gt;: an order will eventually be placed, or the placement will fail. The business says the same sentence.&lt;/p&gt;

&lt;p&gt;The type vocabulary is small and the semantic payload is large -- because every element of it is a domain word. Minimal and expressive stop being opposites when every character in the signature is chosen for domain meaning.&lt;/p&gt;

&lt;h3&gt;
  
  
  Six Patterns, One Vocabulary
&lt;/h3&gt;

&lt;p&gt;Types say what values are. They do not say how computations are shaped. For shape, JBCT names six recurring structures -- the forms that business processes take when written down in code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Leaf&lt;/strong&gt; -- the smallest atomic step, one that can't be broken down into substeps (a value object constructed from raw input, a domain calculation, a database call, an HTTP request)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sequencer&lt;/strong&gt; -- a sequence of steps where each depends on the previous&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fork-Join&lt;/strong&gt; -- independent steps run together, combined when all complete&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Condition&lt;/strong&gt; -- branching based on a decision&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Iteration&lt;/strong&gt; -- the same step applied to many inputs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aspects&lt;/strong&gt; -- observational wrapping (logging, tracing, metrics) around another step&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are not invented abstractions. They are the shapes business processes actually take, recognizable to anyone who has drawn a BPMN diagram or mapped a workflow on a whiteboard. When programmer and stakeholder discuss a feature, they already use this vocabulary implicitly. JBCT makes it explicit in code: a Sequencer of three steps reads as "first this, then this, then this." A Fork-Join reads as "these two things, in parallel, then combine."&lt;/p&gt;

&lt;p&gt;Most functional styles express structure implicitly, through chains of combinators -- the operators carry the shape, the reader has to reconstruct it. JBCT names the shape first and lets the combinators drop into background plumbing. "Sequencer of three, the second of which is a Fork-Join" is a sentence the programmer speaks and the stakeholder understands. After a week of reading JBCT code, the combinators underneath fade to ambient detail; what remains in attention is the process shape.&lt;/p&gt;

&lt;p&gt;Each pattern carries semantic weight, not just structural shape. A Sequencer is a workflow where each step depends on the previous. A Condition is a domain decision point. An Iteration is a rule applied to many participants. Aspects are cross-cutting concerns -- audit, observability, authorization -- the business already thinks of separately from the process they wrap.&lt;/p&gt;

&lt;p&gt;The whole approach is shaped around a single design intent: every structural element should carry business meaning. Types name domain modalities. Patterns name domain process shapes. Value objects name domain concepts and enforce domain invariants. Leaf interfaces name domain operations. The intent doesn't always land perfectly -- some elements resist the reading, some boundaries are hard to draw -- but the resistance becomes a signal: wrong name, wrong place, or something that belongs on the technical side of a Leaf.&lt;/p&gt;

&lt;h3&gt;
  
  
  Leaves and the Quarantine Principle
&lt;/h3&gt;

&lt;p&gt;Leaves are atomic. Some of them are pure in-process operations -- a value object built from raw input, a price calculated from a rate card, a rule evaluated against a policy. These leaves are already in the business's language; there is nothing to quarantine.&lt;/p&gt;

&lt;p&gt;The other leaves cross into something external -- a database, an HTTP service, a message bus, a clock. This is where technical detail wants to leak into business code. JBCT's answer is a naming rule: a leaf that crosses a boundary is declared as an interface named for what the &lt;em&gt;business&lt;/em&gt; needs, not what the &lt;em&gt;technology&lt;/em&gt; provides. Not &lt;code&gt;PostgresUserDao&lt;/code&gt;. Not &lt;code&gt;HttpOrderClient&lt;/code&gt;. &lt;code&gt;UserRepository&lt;/code&gt;. &lt;code&gt;OrderPlacement&lt;/code&gt;. The interface is a domain statement; the implementation behind it is an implementation.&lt;/p&gt;

&lt;p&gt;This is why the business code doesn't need effect tracking in types. The effects don't appear in business code at all. They appear on the other side of named boundary leaves. The business layer reads as a composition of named business operations -- some atomic, some composed. The technical layer reads as a set of domain-named interfaces with technical implementations behind each. The two layers meet at the interface, and the interface speaks the business's language.&lt;/p&gt;

&lt;p&gt;Three containers, six patterns, one boundary primitive. The whole vocabulary fits on a napkin and carries enterprise complexity.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Convergence
&lt;/h3&gt;

&lt;p&gt;This approach did not arrive fully formed. Practitioners in different ecosystems -- working independently, without coordination -- have been arriving at shapes of it for years. &lt;a href="https://dev.to/siy/the-quiet-consensus-5hhk"&gt;Six of them, across F#, Rust, TypeScript, Scala, C#, and Java, describe the same structural adaptation with different vocabulary&lt;/a&gt;. The phrasing varies; the underlying move is the same: processes as the unit of decomposition, types as domain statements, technical detail pushed behind named boundaries.&lt;/p&gt;

&lt;p&gt;JBCT is one formalization of that convergence, in Java, with the containers and patterns named so that teams can talk about them without reinventing the vocabulary each time.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Compiler as Participant
&lt;/h3&gt;

&lt;p&gt;What changes when the type system speaks the domain's language is the role of the compiler. In most type-system discourse the compiler is an overseer of machine correctness -- it catches wrong-shaped values, prevents null dereferences, enforces invariants that the machine cares about. The compiler is a gate.&lt;/p&gt;

&lt;p&gt;When the type system speaks the business's language, the compiler becomes something else. It becomes a participant in the domain conversation. A signature that says &lt;code&gt;Result&amp;lt;Customer&amp;gt; lookup(CustomerId id)&lt;/code&gt; is the code saying aloud, "I can fail to find this customer." If the caller forgets to handle the failure branch, the compiler points out that the domain has a case the caller hasn't addressed. It is not catching a machine error. It is asking the author to finish the domain sentence.&lt;/p&gt;

&lt;p&gt;That is the convergence that matters. Technical and domain languages stop being separate vocabularies that need to be translated at every boundary. They become the same language, spoken by the programmer, read by the stakeholder, enforced by the compiler. The gap the two traditions tried to close from opposite sides closes when the middle stops being empty.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;JBCT is documented at &lt;a href="https://pragmatica.dev" rel="noopener noreferrer"&gt;pragmatica.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>softwaredesign</category>
      <category>java</category>
      <category>functional</category>
      <category>domaindrivendesign</category>
    </item>
    <item>
      <title>Java Backend Design Technology: A Process-First Methodology</title>
      <dc:creator>Sergiy Yevtushenko</dc:creator>
      <pubDate>Mon, 13 Apr 2026 21:53:25 +0000</pubDate>
      <link>https://dev.to/siy/java-backend-design-technology-a-process-first-methodology-2p4m</link>
      <guid>https://dev.to/siy/java-backend-design-technology-a-process-first-methodology-2p4m</guid>
      <description>&lt;h1&gt;
  
  
  Java Backend Design Technology: A Process-First Methodology
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;From Requirements to Code in Eight Questions&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every backend system is a collection of processes, not a collection of entities.&lt;/p&gt;

&lt;p&gt;This sounds obvious when stated plainly. Yet the dominant design methodology for twenty years has been entity-first: identify User, Order, Product. Define their attributes. Attach behavior. Build services that operate on shared objects. The result is a system where every feature is coupled to every other feature through shared data models — and where every new requirement triggers an architecture discussion.&lt;/p&gt;

&lt;p&gt;A different approach is gaining traction. Independent practitioners across five languages — F#, Rust, Java, Scala, .NET — have independently converged on the same structural insight: start with behavior, derive data. Design around what the system &lt;em&gt;does&lt;/em&gt;, not what it &lt;em&gt;stores&lt;/em&gt;. Six of them are documented in &lt;a href="https://dev.to/siy/the-quiet-consensus-5hhk"&gt;The Quiet Consensus&lt;/a&gt;, but the pattern extends far beyond those six.&lt;/p&gt;

&lt;p&gt;Structured programming eliminated goto debates by making control flow mechanical. This methodology does the same for design: it makes most architectural decisions mechanical, determined by the problem rather than by the developer's preferences.&lt;/p&gt;

&lt;p&gt;This article presents &lt;strong&gt;Java Backend Design Technology&lt;/strong&gt; (JBDT) — the design phase of &lt;a href="https://pragmatica.dev/" rel="noopener noreferrer"&gt;Java Backend Coding Technology&lt;/a&gt; (JBCT). It's a concrete, repeatable process for going from requirements to code structure. No entity diagrams. No aggregate boundaries. No architecture review boards. Just eight questions and the types that emerge from the answers.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Questions Framework
&lt;/h2&gt;

&lt;p&gt;For any feature, ask these eight questions. The answers produce the code structure mechanically.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;What triggers this process?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What data does it need?&lt;/strong&gt; — this becomes the Request record&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What does success look like?&lt;/strong&gt; — this becomes the Response record&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What can go wrong?&lt;/strong&gt; — these become error types (sealed interface with enum for fixed messages, records for contextual errors)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What are the steps?&lt;/strong&gt; — these become step interfaces&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Which steps depend on each other?&lt;/strong&gt; — dependencies become sequential chains; independent steps become parallel operations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Are there conditional paths?&lt;/strong&gt; — these become branching logic&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is there collection processing?&lt;/strong&gt; — this becomes iteration&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's the entire design process. No other questions need answering. Pattern selection, type placement, error strategy — all determined by these eight answers.&lt;/p&gt;

&lt;p&gt;Let's see it work.&lt;/p&gt;




&lt;h2&gt;
  
  
  Applying the Framework: Place an Order
&lt;/h2&gt;

&lt;p&gt;Requirements: a customer places an order with items, a shipping address, and a payment method. The system checks inventory, processes payment, creates the order, and sends confirmation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question 1 — What triggers this?&lt;/strong&gt; A customer submits an order.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question 2 — What data does it need?&lt;/strong&gt; Customer ID, list of items with quantities, shipping address, payment method.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="nf"&gt;Request&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;customerId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;OrderItem&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
               &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;shippingAddress&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;paymentMethod&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt;
&lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="nf"&gt;OrderItem&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;productId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Question 3 — What does success look like?&lt;/strong&gt; An order confirmation with an ID and estimated delivery.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="nf"&gt;Response&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;LocalDate&lt;/span&gt; &lt;span class="n"&gt;estimatedDelivery&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Question 4 — What can go wrong?&lt;/strong&gt; Invalid inputs, insufficient inventory, payment declined.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;sealed&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;PlaceOrderError&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Cause&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;enum&lt;/span&gt; &lt;span class="nc"&gt;General&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;PlaceOrderError&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="no"&gt;EMPTY_CART&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Cart is empty"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
        &lt;span class="no"&gt;INVALID_ADDRESS&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Shipping address is invalid"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="nf"&gt;InsufficientInventory&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;productId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;requested&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;available&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;PlaceOrderError&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="nf"&gt;PaymentDeclined&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;PlaceOrderError&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Question 5 — What are the steps?&lt;/strong&gt; Validate the request. Check inventory. Process payment. Create order. Send confirmation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question 6 — Which steps depend on each other?&lt;/strong&gt; Validation must happen first. Inventory check and payment processing are independent of each other. Order creation depends on both succeeding. Confirmation depends on the order being created.&lt;/p&gt;

&lt;p&gt;This directly produces the composition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;PlaceOrder&lt;/span&gt; &lt;span class="nf"&gt;placeOrder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CheckInventory&lt;/span&gt; &lt;span class="n"&gt;checkInventory&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                             &lt;span class="nc"&gt;ProcessPayment&lt;/span&gt; &lt;span class="n"&gt;processPayment&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                             &lt;span class="nc"&gt;CreateOrder&lt;/span&gt; &lt;span class="n"&gt;createOrder&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                             &lt;span class="nc"&gt;SendConfirmation&lt;/span&gt; &lt;span class="n"&gt;sendConfirmation&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;ValidRequest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;validRequest&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                                  &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;async&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                                  &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;flatMap&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;valid&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;reserveOrder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;checkInventory&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;processPayment&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;valid&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
                                  &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;flatMap&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;createOrder:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;apply&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                                  &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;flatMap&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;sendConfirmation:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;apply&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ReservedOrder&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;reserveOrder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CheckInventory&lt;/span&gt; &lt;span class="n"&gt;checkInventory&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                                                   &lt;span class="nc"&gt;ProcessPayment&lt;/span&gt; &lt;span class="n"&gt;processPayment&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                                                   &lt;span class="nc"&gt;ValidRequest&lt;/span&gt; &lt;span class="n"&gt;valid&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;all&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;checkInventory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;apply&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;valid&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
                       &lt;span class="n"&gt;processPayment&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;apply&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;valid&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
                  &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;ReservedOrder:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Questions 7 and 8&lt;/strong&gt; — no conditional paths or collection processing in the main flow.&lt;/p&gt;

&lt;p&gt;Notice what happened. We didn't draw a class diagram. We didn't debate which aggregate owns what. We didn't create a shared &lt;code&gt;Order&lt;/code&gt; entity used by every feature. We asked eight questions, wrote down the answers, and the code structure emerged.&lt;/p&gt;

&lt;p&gt;Notice also: &lt;code&gt;ValidateCart&lt;/code&gt;, &lt;code&gt;ProcessPayment&lt;/code&gt;, &lt;code&gt;InsufficientInventory&lt;/code&gt;, &lt;code&gt;PaymentDeclined&lt;/code&gt; — these are exactly the words a domain expert would use when describing this process. The shared vocabulary between developers and business emerged directly from the design process, without dedicated modeling sessions. This is DDD's ubiquitous language in practice — emerging naturally rather than being constructed.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Second Example: Publish Article
&lt;/h2&gt;

&lt;p&gt;To show this isn't specific to e-commerce, let's design a content publishing feature.&lt;/p&gt;

&lt;p&gt;Requirements: an author submits an article. The system validates the content, checks for duplicates, generates a slug, and publishes to multiple platforms.&lt;/p&gt;

&lt;p&gt;Walking through the questions quickly:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Trigger:&lt;/strong&gt; Author submits article&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data needed:&lt;/strong&gt; Title, body, tags, author ID, target platforms&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Success:&lt;/strong&gt; Published URLs for each platform&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failures:&lt;/strong&gt; Invalid content, duplicate title, platform rejection&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Steps:&lt;/strong&gt; Validate → check duplicates → generate slug → publish to platforms&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dependencies:&lt;/strong&gt; Validation and duplicate check are independent. Slug generation depends on both. Publishing to each platform is independent (parallel).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conditional paths:&lt;/strong&gt; If a platform rejects, continue with others (best-effort)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Collection processing:&lt;/strong&gt; Publishing to multiple platforms
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;PublishArticle&lt;/span&gt; &lt;span class="nf"&gt;publishArticle&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ValidateContent&lt;/span&gt; &lt;span class="n"&gt;validate&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                                     &lt;span class="nc"&gt;CheckDuplicates&lt;/span&gt; &lt;span class="n"&gt;checkDups&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                                     &lt;span class="nc"&gt;GenerateSlug&lt;/span&gt; &lt;span class="n"&gt;generateSlug&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                                     &lt;span class="nc"&gt;PlatformPublisher&lt;/span&gt; &lt;span class="n"&gt;publisher&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;validateArticle&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;validate&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;checkDups&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                          &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;flatMap&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;generateSlug:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;apply&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                          &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;flatMap&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;article&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;publisher&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;publishAll&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;article&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;platforms&lt;/span&gt;&lt;span class="o"&gt;()));&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ValidArticle&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;validateArticle&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ValidateContent&lt;/span&gt; &lt;span class="n"&gt;validate&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                                                     &lt;span class="nc"&gt;CheckDuplicates&lt;/span&gt; &lt;span class="n"&gt;checkDups&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                                                     &lt;span class="nc"&gt;Request&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;all&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;validate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;apply&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
                       &lt;span class="n"&gt;checkDups&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;apply&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
                  &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;ValidArticle:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Different domain, same eight questions, same mechanical process. The structure is determined by the answers, not by preferences.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why It Works: Processes as Knowledge Gathering
&lt;/h2&gt;

&lt;p&gt;There's a deeper structure underneath the eight questions.&lt;/p&gt;

&lt;p&gt;Every backend process is fundamentally an act of &lt;strong&gt;knowledge gathering&lt;/strong&gt;. Each step acquires a piece of knowledge. The process ends — successfully or not — when enough knowledge has accumulated to formulate an answer.&lt;/p&gt;

&lt;p&gt;In PlaceOrder:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Validation&lt;/strong&gt; gathers knowledge: "the inputs are well-formed"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inventory check&lt;/strong&gt; gathers knowledge: "the items are available"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payment processing&lt;/strong&gt; gathers knowledge: "the funds are secured"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Order creation&lt;/strong&gt; gathers knowledge: "the order is persisted"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A failure at any step is also knowledge. A declined payment tells the process "funds are not available" — and that's enough to formulate the answer "order cannot be placed." The process doesn't need to continue once it has enough knowledge to respond.&lt;/p&gt;

&lt;p&gt;This reframes data modeling entirely. Instead of asking "what data exists in the system?" (which produces entity diagrams), you ask "what does this process need to know?" The first question leads to shared entities. The second leads to per-process types — exactly what the methodology produces.&lt;/p&gt;




&lt;h2&gt;
  
  
  Data Dependency Graphs
&lt;/h2&gt;

&lt;p&gt;The knowledge-gathering view has a formal structure. Three operators describe how pieces of knowledge relate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sequential&lt;/strong&gt; — need A before gathering B. "Validate first, then check inventory."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ALL(A, B)&lt;/strong&gt; — need both, they're independent. "Check inventory AND process payment."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ANY(A, B)&lt;/strong&gt; — either source suffices. "Get credit score from internal system OR external bureau."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Between operators, transformation functions convert one piece of knowledge into another — pure business logic.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Detailed Example: Resolve Customer Credit
&lt;/h3&gt;

&lt;p&gt;A lending system needs to make a credit decision. The process:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Attempt to get the customer's credit score from the internal scoring system&lt;/li&gt;
&lt;li&gt;If internal scoring is unavailable, fall back to an external credit bureau&lt;/li&gt;
&lt;li&gt;Independently, retrieve the customer's payment history&lt;/li&gt;
&lt;li&gt;Combine credit score and payment history to calculate a risk assessment&lt;/li&gt;
&lt;li&gt;Apply lending policy to produce the final decision&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;As a data dependency graph:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CreditDecision = ApplyPolicy(
                     Assess(
                         ALL(
                             ANY(InternalScore, ExternalBureau),
                             PaymentHistory
                         )
                     )
                 )
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reading from the inside out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ANY(InternalScore, ExternalBureau)&lt;/code&gt; — gather credit score from whichever source responds successfully first, any response is equally correct.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ALL(..., PaymentHistory)&lt;/code&gt; — gather the credit score (from either source) AND the payment history independently, in parallel.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Assess(...)&lt;/code&gt; — transform both pieces of knowledge into a risk assessment. Pure function, no I/O.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ApplyPolicy(...)&lt;/code&gt; — transform the risk assessment into a lending decision. Pure function.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This maps directly to code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;ResolveCredit&lt;/span&gt; &lt;span class="nf"&gt;resolveCredit&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;InternalScoring&lt;/span&gt; &lt;span class="n"&gt;internal&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                                   &lt;span class="nc"&gt;ExternalBureau&lt;/span&gt; &lt;span class="n"&gt;external&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                                   &lt;span class="nc"&gt;PaymentHistoryService&lt;/span&gt; &lt;span class="n"&gt;history&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                                   &lt;span class="nc"&gt;RiskAssessor&lt;/span&gt; &lt;span class="n"&gt;assessor&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                                   &lt;span class="nc"&gt;LendingPolicy&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;gatherCreditData&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;internal&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;external&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;history&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                          &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;assessor:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;assess&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                          &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;policy:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;apply&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CreditData&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;gatherCreditData&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;InternalScoring&lt;/span&gt; &lt;span class="n"&gt;internal&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                                                    &lt;span class="nc"&gt;ExternalBureau&lt;/span&gt; &lt;span class="n"&gt;external&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                                                    &lt;span class="nc"&gt;PaymentHistoryService&lt;/span&gt; &lt;span class="n"&gt;history&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                                                    &lt;span class="nc"&gt;Request&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;all&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obtainCreditScore&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;internal&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;external&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
                       &lt;span class="n"&gt;history&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;retrieve&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;customerId&lt;/span&gt;&lt;span class="o"&gt;()))&lt;/span&gt;
                  &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;CreditData:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CreditScore&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;obtainCreditScore&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;InternalScoring&lt;/span&gt; &lt;span class="n"&gt;internal&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                                                      &lt;span class="nc"&gt;ExternalBureau&lt;/span&gt; &lt;span class="n"&gt;external&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                                                      &lt;span class="nc"&gt;Request&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;internal&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;score&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                   &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;orElse&lt;/span&gt;&lt;span class="o"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;external&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;score&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three methods, one pattern each. &lt;code&gt;resolveCredit&lt;/code&gt; is the Sequencer (gather → assess → decide). &lt;code&gt;gatherCreditData&lt;/code&gt; is the Fork-Join — the &lt;code&gt;ALL&lt;/code&gt; operator gathering independent pieces of knowledge in parallel. &lt;code&gt;obtainCreditScore&lt;/code&gt; is the fallback — the &lt;code&gt;ANY&lt;/code&gt; operator trying the internal source first, falling back to external. The sequential &lt;code&gt;.map&lt;/code&gt; calls are transformations that convert gathered knowledge into the final answer.&lt;/p&gt;

&lt;p&gt;The DDG notation captures more useful information than an entity-relationship diagram. An ER diagram tells you what data exists. A DDG tells you what a process needs to know, where it gets that knowledge, and what depends on what. The code writes itself from the graph.&lt;/p&gt;




&lt;h2&gt;
  
  
  When Requirements Change
&lt;/h2&gt;

&lt;p&gt;The product owner says: "We need to check for fraud before processing payment."&lt;/p&gt;

&lt;p&gt;In an entity-first design, this triggers questions: Does the Order entity need a fraud status? Where does the fraud check live in the service layer? Do we need a new aggregate?&lt;/p&gt;

&lt;p&gt;In process-first design, the response is mechanical:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add a new step interface: &lt;code&gt;CheckFraud&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Ask question 6: does fraud check depend on other steps? It needs the validated request — so it comes after validation.&lt;/li&gt;
&lt;li&gt;Is it independent of other steps? It's independent of inventory check but payment should wait for it.&lt;/li&gt;
&lt;li&gt;Insert it into the composition:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;PlaceOrder&lt;/span&gt; &lt;span class="nf"&gt;placeOrder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CheckInventory&lt;/span&gt; &lt;span class="n"&gt;checkInventory&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                             &lt;span class="nc"&gt;ProcessPayment&lt;/span&gt; &lt;span class="n"&gt;processPayment&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                             &lt;span class="nc"&gt;CreateOrder&lt;/span&gt; &lt;span class="n"&gt;createOrder&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                             &lt;span class="nc"&gt;SendConfirmation&lt;/span&gt; &lt;span class="n"&gt;sendConfirmation&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                             &lt;span class="nc"&gt;CheckFraud&lt;/span&gt; &lt;span class="n"&gt;checkFraud&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;ValidRequest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;validRequest&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                                  &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;async&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                                  &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;flatMap&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;valid&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;reserveOrder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;checkInventory&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;processPayment&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;checkFraud&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;valid&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
                                  &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;flatMap&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;createOrder:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;apply&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                                  &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;flatMap&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;sendConfirmation:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;apply&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ReservedOrder&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;reserveOrder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CheckInventory&lt;/span&gt; &lt;span class="n"&gt;checkInventory&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                                                   &lt;span class="nc"&gt;ProcessPayment&lt;/span&gt; &lt;span class="n"&gt;processPayment&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                                                   &lt;span class="nc"&gt;CheckFraud&lt;/span&gt; &lt;span class="n"&gt;checkFraud&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                                                   &lt;span class="nc"&gt;ValidRequest&lt;/span&gt; &lt;span class="n"&gt;valid&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;all&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;checkInventory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;apply&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;valid&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
                       &lt;span class="n"&gt;verifyAndPay&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;checkFraud&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;processPayment&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;valid&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
                  &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;ReservedOrder:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PaymentResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;verifyAndPay&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CheckFraud&lt;/span&gt; &lt;span class="n"&gt;checkFraud&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                                                   &lt;span class="nc"&gt;ProcessPayment&lt;/span&gt; &lt;span class="n"&gt;processPayment&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                                                   &lt;span class="nc"&gt;ValidRequest&lt;/span&gt; &lt;span class="n"&gt;valid&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;checkFraud&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;apply&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;valid&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                     &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;flatMap&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;processPayment&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;apply&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;valid&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One new step interface. One change to the composition. No restructuring. No entity changes. No architecture discussion.&lt;/p&gt;

&lt;p&gt;This is what mechanical design evolution looks like: patterns are used from the start, so evolution is adding and recomposing — never restructuring.&lt;/p&gt;




&lt;h2&gt;
  
  
  When Processes Meet Persistence
&lt;/h2&gt;

&lt;p&gt;"But what about the database? Don't you end up with entities anyway?"&lt;/p&gt;

&lt;p&gt;Yes — and that's fine. The difference is how they get there.&lt;/p&gt;

&lt;p&gt;Multiple use cases converge on the same database tables. &lt;code&gt;PlaceOrder&lt;/code&gt; writes order rows. &lt;code&gt;CancelOrder&lt;/code&gt; updates the status column. &lt;code&gt;TrackOrder&lt;/code&gt; reads shipping info. &lt;code&gt;GenerateInvoice&lt;/code&gt; reads billing fields. The &lt;code&gt;orders&lt;/code&gt; table is the union of what these processes need.&lt;/p&gt;

&lt;p&gt;But this entity &lt;strong&gt;emerged&lt;/strong&gt; from process convergence. It wasn't designed upfront. Every column has a known consumer — the process that needed it. No speculative fields "just in case." When a new process needs something new, you add it, and you know exactly why.&lt;/p&gt;

&lt;p&gt;The key insight: &lt;strong&gt;entities are discovered, not invented.&lt;/strong&gt; They're the natural intersection of processes that share persistence — a composition of views, not a universal model.&lt;/p&gt;

&lt;p&gt;This isn't always the case. In event-sourced systems, entities may never materialize into a single flat record. Each process folds the event stream into exactly the shape it needs — different state reconstructions for different contexts (Rico Fritzsche explores this in depth in &lt;a href="https://levelup.gitconnected.com/beyond-aggregates-lean-functional-event-sourcing-1f008cf236fc" rel="noopener noreferrer"&gt;Beyond Aggregates&lt;/a&gt;). Whether your entities live as database rows, event folds, or CQRS projections, the principle holds: they're composed from process needs, not designed ahead of time.&lt;/p&gt;




&lt;h2&gt;
  
  
  Practical Consequences
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Design meetings get shorter.&lt;/strong&gt; When the methodology is mechanical, there's less to debate. "What are the use cases? What are the answers to the eight questions?" produces a design in minutes, not hours. Architects are freed to focus on genuinely hard problems — infrastructure, scaling, cross-system integration — instead of mediating aggregate boundary debates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The six JBCT structural patterns map directly to BPMN constructs&lt;/strong&gt; — code written using these patterns is structurally equivalent to a business process diagram. For the full pattern-BPMN mapping, see the &lt;a href="https://pragmatica.dev/" rel="noopener noreferrer"&gt;JBCT series&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Testing becomes a side effect of design.&lt;/strong&gt; Every step interface is a test seam. Stub it, test the composition. The design produces testable code by construction — no special effort required.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI-assisted development benefits directly.&lt;/strong&gt; Given the eight questions answered, an AI assistant produces structurally correct code because the design space is fully constrained. There's essentially one valid structure for a given set of answers.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;Pick a feature from your current project. Ask the eight questions. Write down the Request, Response, error types, and step interfaces before writing any implementation code.&lt;/p&gt;

&lt;p&gt;Notice how the structure emerges from the answers — not from architectural decisions or entity modeling. Notice how the types you name are the words your domain expert would use. Notice how you didn't need to debate anything.&lt;/p&gt;

&lt;p&gt;That's JBDT in practice.&lt;/p&gt;




&lt;h2&gt;
  
  
  Going Deeper
&lt;/h2&gt;

&lt;p&gt;Java Backend Design Technology is the design phase of &lt;a href="https://pragmatica.dev/" rel="noopener noreferrer"&gt;Java Backend Coding Technology&lt;/a&gt; — a complete methodology covering design, implementation patterns, testing strategy, and tooling. JBDT has been validated against a 326,000-line distributed runtime (&lt;a href="https://github.com/pragmaticalabs/pragmatica" rel="noopener noreferrer"&gt;Aether&lt;/a&gt;) and formalized into a &lt;a href="https://pragmatica.dev/" rel="noopener noreferrer"&gt;comprehensive learning series&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you've independently arrived at similar conclusions from your own domain, language, or tradition — I'd like to hear about it. The convergence is the interesting part.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Further reading:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dev.to/siy/the-quiet-consensus-5hhk"&gt;The Quiet Consensus&lt;/a&gt; — the convergent evolution toward process-first design&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://medium.com/swlh/hidden-anatomy-of-backend-applications-data-dependencies-5e4ce735b0e1" rel="noopener noreferrer"&gt;Hidden Anatomy of Backend Applications: Data Dependencies&lt;/a&gt; — the DDG formalism&lt;/li&gt;
&lt;li&gt;Scott Wlaschin, &lt;em&gt;&lt;a href="https://pragprog.com/titles/swdddf/domain-modeling-made-functional/" rel="noopener noreferrer"&gt;Domain Modeling Made Functional&lt;/a&gt;&lt;/em&gt; — workflows with typed boundaries in F#&lt;/li&gt;
&lt;li&gt;Rico Fritzsche, &lt;em&gt;&lt;a href="https://levelup.gitconnected.com/beyond-aggregates-lean-functional-event-sourcing-1f008cf236fc" rel="noopener noreferrer"&gt;Beyond Aggregates: Lean, Functional Event Sourcing&lt;/a&gt;&lt;/em&gt; — aggregateless design with event sourcing&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>java</category>
      <category>architecture</category>
      <category>design</category>
      <category>programming</category>
    </item>
    <item>
      <title>The Quiet Consensus</title>
      <dc:creator>Sergiy Yevtushenko</dc:creator>
      <pubDate>Sun, 12 Apr 2026 10:27:18 +0000</pubDate>
      <link>https://dev.to/siy/the-quiet-consensus-5hhk</link>
      <guid>https://dev.to/siy/the-quiet-consensus-5hhk</guid>
      <description>&lt;p&gt;Something is happening in software design that nobody organized.&lt;/p&gt;

&lt;p&gt;Practitioners from different languages, different domains, different traditions are arriving at the same conclusion — independently, without coordination, often without knowing about each other's work.&lt;/p&gt;

&lt;p&gt;The conclusion: &lt;strong&gt;business processes, not data entities, are the natural unit of software decomposition.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This isn't a manifesto. There was no conference keynote. No working group. Just a growing body of work from people solving real problems who kept ending up in the same place.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Standard Starting Point
&lt;/h3&gt;

&lt;p&gt;For two decades, the dominant approach to backend design has been data-first. Identify the entities — User, Order, Product. Define their attributes and relationships. Attach behavior. Build services that operate on shared objects.&lt;/p&gt;

&lt;p&gt;This approach, formalized in Domain-Driven Design's tactical patterns, produces shared entity models that serve multiple contexts. The coupling is structural: change a shared entity, and every consumer is affected. Add a field to Order, and every service that touches orders must accommodate it — even services that don't care about the new field.&lt;/p&gt;

&lt;p&gt;The pattern works. It has built enormous systems. But practitioners working at scale report the same friction points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Entities grow into God objects because every feature needs something different from the same concept&lt;/li&gt;
&lt;li&gt;Mapping layers accumulate (DTO to entity to DTO) because the entity doesn't fit any single feature perfectly&lt;/li&gt;
&lt;li&gt;Architecture discussions become debates about aggregate boundaries — who owns what, how big should the aggregate be, where does this behavior belong&lt;/li&gt;
&lt;li&gt;The "where does domain logic live?" question never settles — rich domain model, anemic domain model, transaction scripts, domain services. Each team picks differently, each project mixes approaches, and the answer changes depending on who you ask. The debate persists because entity-first design doesn't have a natural home for behavior that spans multiple entities&lt;/li&gt;
&lt;li&gt;Coupling increases with every new feature because features share entities instead of owning their own types&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These aren't implementation failures. They're structural consequences of starting with data.&lt;/p&gt;

&lt;h3&gt;
  
  
  Convergent Evolution
&lt;/h3&gt;

&lt;p&gt;In biology, convergent evolution describes species that develop the same trait independently — wings in birds, bats, and insects. Different lineages, different mechanisms, same solution to the same problem.&lt;/p&gt;

&lt;p&gt;Something similar is happening in software design. Practitioners from different ecosystems are converging on the same structural adaptation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scott Wlaschin&lt;/strong&gt; (F#) models domains as workflows with typed inputs and outputs, composing small functions into complete use cases. His phrase "make illegal states unrepresentable" captures the type-driven approach. His book &lt;a href="https://pragprog.com/titles/swdddf/domain-modeling-made-functional/" rel="noopener noreferrer"&gt;&lt;em&gt;Domain Modeling Made Functional&lt;/em&gt;&lt;/a&gt; demonstrates how types replace defensive coding and how workflows replace entity models.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rico Fritzsche&lt;/strong&gt; (Rust/TypeScript) models domains as contextual decisions, not shared entities. Each feature slice owns its own state reconstruction. In his framing, entities are not fixed structures — they are &lt;a href="https://levelup.gitconnected.com/how-to-model-domain-logic-without-shared-entities-05c938eee73f" rel="noopener noreferrer"&gt;"flexible, context-dependent manifestations"&lt;/a&gt;. A "Seat" is a row and number in booking, a reservation status in availability, a price category in pricing. Three different types, three different processes, no shared entity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Roman Weis&lt;/strong&gt; (Java) proposes focusing &lt;a href="https://medium.com/codex/lets-build-business-software-an-alternative-approach-to-the-standard-ddd-implementation-47e586b5f81f" rel="noopener noreferrer"&gt;"100% on behavior — the commands"&lt;/a&gt; instead of finding the perfect aggregate root. Business logic belongs in scoped, task-based commands.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sandro Mancuso&lt;/strong&gt; (Java/Craftsmanship) starts from external usage. His &lt;a href="https://www.codurance.com/publications/2017/12/08/introducing-idd" rel="noopener noreferrer"&gt;Interaction-Driven Design&lt;/a&gt; lets the domain model emerge from actual needs — use cases first, internal structure second. The domain isn't modeled in advance; it's discovered through implementation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Jimmy Bogard&lt;/strong&gt; (.NET) organizes code by features, not layers. His &lt;a href="https://www.jimmybogard.com/vertical-slice-architecture/" rel="noopener noreferrer"&gt;Vertical Slice Architecture&lt;/a&gt; minimizes coupling between slices and maximizes coupling within a slice. Each feature is self-contained. Shared abstractions are extracted only when proven necessary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Debasish Ghosh&lt;/strong&gt; (Scala) expresses behavior as pure function compositions rather than object methods, with immutable types and explicit side effects. His book &lt;a href="https://www.manning.com/books/functional-and-reactive-domain-modeling" rel="noopener noreferrer"&gt;&lt;em&gt;Functional and Reactive Domain Modeling&lt;/em&gt;&lt;/a&gt; shows how algebraic types and composition replace the entity-service-repository pattern.&lt;/p&gt;

&lt;p&gt;Six practitioners. Five languages. Different continents, different communities, different audiences. None of them cites the others as primary inspiration. They arrived at the same place because they were solving the same problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  What They Share
&lt;/h3&gt;

&lt;p&gt;Strip away the language-specific details and the individual vocabulary, and the shared structure becomes clear:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Processes over entities.&lt;/strong&gt; The primary decomposition unit is a business operation with a trigger, inputs, outputs, and failure modes — not a data entity with attributes and relationships.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Per-context types.&lt;/strong&gt; Data structures are shaped by the process that uses them. A "User" in registration has different fields than a "User" in authentication or billing. Each process owns its own types.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Functional composition.&lt;/strong&gt; Small, pure, testable operations composed into larger workflows. The composition itself is the design — not a layer on top of a design.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No shared domain model.&lt;/strong&gt; Domain knowledge is distributed across processes, not centralized in entity classes. Shared types exist only for validated domain concepts (email addresses, monetary amounts) that genuinely mean the same thing across contexts.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's Driving the Convergence
&lt;/h3&gt;

&lt;p&gt;Independent convergence implies shared environmental pressure. Several forces are pushing practitioners toward process-first design simultaneously:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Distributed systems demand it.&lt;/strong&gt; Microservices and serverless architectures naturally align with process-based decomposition. Each service IS a process. Trying to maintain shared entity models across service boundaries creates the exact coupling that microservices were supposed to eliminate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scale reveals entity-model friction.&lt;/strong&gt; Small systems don't feel the pain of shared entities. At 5 services, a shared User entity is manageable. At 50, it's a coordination bottleneck. At 500, it's impossible. Teams that grow past a certain size independently discover that process boundaries work better than entity boundaries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Functional programming went mainstream.&lt;/strong&gt; Rust brought algebraic types, pattern matching, &lt;code&gt;Result&amp;lt;T, E&amp;gt;&lt;/code&gt;, and immutability by default to systems programming — proving these aren't academic preferences but engineering necessities. Java added records, sealed interfaces, and pattern matching. C# added records and discriminated unions. TypeScript refined literal types and discriminated unions. The languages now support typed composition natively, without framework overhead. What was theoretical in 2010 is practical in 2025.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI-assisted development rewards deterministic patterns.&lt;/strong&gt; When the design process is mechanical — ask these questions, the structure follows — AI can participate reliably. Process-first design constrains the design space in ways that make AI-generated code structurally correct more often. This wasn't a design goal for any of the practitioners cited. It's an emergent benefit.&lt;/p&gt;

&lt;h3&gt;
  
  
  From Observation to Practice
&lt;/h3&gt;

&lt;p&gt;Observing convergence is interesting. Formalizing it is useful.&lt;/p&gt;

&lt;p&gt;If processes are the natural decomposition unit, the design activity becomes identifying processes and their boundaries. A process has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Typed input&lt;/strong&gt; — what triggers it and what data it needs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Typed output&lt;/strong&gt; — what success looks like&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Typed failures&lt;/strong&gt; — what can go wrong&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Steps&lt;/strong&gt; — sub-processes with their own typed boundaries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These aren't abstract categories. They're concrete questions you can ask about any feature requirement:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What triggers this process?&lt;/li&gt;
&lt;li&gt;What data does it need?&lt;/li&gt;
&lt;li&gt;What does success look like?&lt;/li&gt;
&lt;li&gt;What can go wrong?&lt;/li&gt;
&lt;li&gt;What are the steps?&lt;/li&gt;
&lt;li&gt;Which steps depend on each other?&lt;/li&gt;
&lt;li&gt;Are there conditional paths?&lt;/li&gt;
&lt;li&gt;Is there collection processing?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The answers determine the code structure. Not guidelines — deterministic mapping. Independent steps become parallel operations. Sequential dependencies become chains. Conditional paths become branches. The developer doesn't invent the structure; they discover it from the answers.&lt;/p&gt;

&lt;p&gt;Consider an order placement process. The questions yield:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Input:&lt;/strong&gt; customer, items, address, payment method&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Output:&lt;/strong&gt; order confirmation with estimated delivery&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failures:&lt;/strong&gt; invalid items, insufficient inventory, payment declined&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Steps:&lt;/strong&gt; validate, check inventory, process payment, create order, send confirmation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dependencies:&lt;/strong&gt; inventory check and payment are independent; order creation depends on both&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The dependency analysis tells you the composition pattern: validate first (sequential), then inventory and payment in parallel (fork-join), then create order (sequential), then confirm (sequential). The code writes itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;validate → (check inventory ∥ process payment) → create order → confirm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No architecture meeting required. No debate about aggregate boundaries. No class diagram. The process structure IS the architecture, derived mechanically from the requirements.&lt;/p&gt;

&lt;h3&gt;
  
  
  Context-Specific Types
&lt;/h3&gt;

&lt;p&gt;One consequence of process-first design deserves emphasis: types belong to processes, not to the domain.&lt;/p&gt;

&lt;p&gt;In entity-first design, you model "Seat" once and every feature uses that model. In process-first design, each process models exactly what it needs:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Booking&lt;/strong&gt; — a location to select (row, number)&lt;br&gt;
&lt;strong&gt;Reservation&lt;/strong&gt; — a time-limited hold (id, reserved until)&lt;br&gt;
&lt;strong&gt;Pricing&lt;/strong&gt; — a cost input (id, category, base price)&lt;/p&gt;

&lt;p&gt;Three different types, three different processes. No shared entity, no conflict, no coupling. Change the pricing model — only the pricing process changes. Add reservation expiry logic — only the reservation process is affected.&lt;/p&gt;

&lt;p&gt;Shared types emerge only when genuinely needed: an email address means the same thing in registration and login, so it becomes a shared value object. But the sharing is discovered from evidence, not designed from speculation.&lt;/p&gt;
&lt;h3&gt;
  
  
  What This Changes
&lt;/h3&gt;

&lt;p&gt;When processes own their types and composition follows from dependency analysis:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The language footprint shrinks.&lt;/strong&gt; Most language features serve entity-model infrastructure — inheritance hierarchies, mutable state, reflection. Process-first design uses a small subset: records for data, sealed interfaces for alternatives, lambdas for composition. The rest becomes unnecessary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code reads like business documentation.&lt;/strong&gt; A process method reads as a sequence of named business operations. New team members learn the domain by reading the code, not the framework.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design evolves mechanically.&lt;/strong&gt; New step? Add a step interface and insert it in the chain. Steps become independent? Change sequential to parallel. Process grows too large? Extract a sub-process. No "refactoring to patterns" — the patterns are used from the start.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Business stakeholders can validate structure.&lt;/strong&gt; When code maps directly to process descriptions, a business analyst can look at the composition and verify it matches the business process. The gap between specification and implementation narrows to zero.&lt;/p&gt;
&lt;h3&gt;
  
  
  What Replaces Entity Modeling
&lt;/h3&gt;

&lt;p&gt;A natural objection: if we don't start with entities, what happens to data modeling?&lt;/p&gt;

&lt;p&gt;It doesn't disappear. It transforms.&lt;/p&gt;

&lt;p&gt;Every backend process is fundamentally an act of knowledge gathering. Check inventory — now you know availability. Process payment — now you know if funds cleared. Each step acquires a piece of knowledge. The process ends — successfully or not — when enough knowledge has accumulated to formulate an answer.&lt;/p&gt;

&lt;p&gt;This reframes data modeling entirely. Instead of asking "what data exists in the system?" (entity diagram), you ask "what does this process need to know?" (dependency graph). The data model becomes a &lt;a href="https://medium.com/swlh/hidden-anatomy-of-backend-applications-data-dependencies-5e4ce735b0e1" rel="noopener noreferrer"&gt;data dependency graph&lt;/a&gt; scoped to each process:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PlaceOrder = Transform(ALL(InventoryStatus, PaymentResult))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ALL&lt;/code&gt; means "I need both pieces of knowledge — they're independent." Sequential chaining means "I need this knowledge before I can gather the next." &lt;code&gt;ANY&lt;/code&gt; means "I can get this knowledge from multiple sources — the first success is enough."&lt;/p&gt;

&lt;p&gt;These operators map directly to composition patterns in code. &lt;code&gt;ALL&lt;/code&gt; is a fork-join. Sequential chaining is a flatMap. &lt;code&gt;ANY&lt;/code&gt; is a fallback. The code structure mirrors the knowledge dependency structure — not because of a design framework, but because gathering knowledge to produce answers is what the code actually does.&lt;/p&gt;

&lt;p&gt;The consequence: data types are scoped to the knowledge a process needs, not to what exists in the database. A "Seat" in the booking process carries row and number. A "Seat" in the pricing process carries category and base price. They're different knowledge, gathered for different answers. No shared entity needed.&lt;/p&gt;

&lt;p&gt;Entity modeling asks: "What is a Seat?" — and produces one answer that fits no process perfectly.&lt;br&gt;
Process modeling asks: "What does this process need to know about seats?" — and produces exactly the right answer for each process.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Relationship to DDD
&lt;/h3&gt;

&lt;p&gt;This isn't a rejection of Domain-Driven Design. DDD's most enduring contributions — bounded contexts, ubiquitous language, the insistence that software should model the domain — remain essential.&lt;/p&gt;

&lt;p&gt;What's being reconsidered is the starting point. DDD's tactical patterns start with entities and aggregates, then attach behavior. Process-first design starts with behavior, then derives the types. The strategic patterns (bounded contexts, context mapping) are fully compatible — in fact, process boundaries often align with context boundaries more naturally than entity boundaries do.&lt;/p&gt;

&lt;p&gt;The ubiquitous language still matters. But in process-first design, it emerges from use case identification and type definition rather than from separate modeling sessions. When a domain expert says "we need to check inventory before processing payment," that sentence maps directly to step interfaces and their ordering. The code reads like the conversation that produced it.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Quiet Part
&lt;/h3&gt;

&lt;p&gt;Nobody organized this convergence. There's no foundation, no standard, no certification program. Just practitioners solving problems and publishing what they found.&lt;/p&gt;

&lt;p&gt;That's what makes it credible. When one person proposes a new methodology, it's an opinion. When six people from different ecosystems independently arrive at the same methodology, it's a signal. The environmental pressures — distributed systems, team scaling, AI-assisted development, functional language features — are producing the same structural adaptation across the industry.&lt;/p&gt;

&lt;p&gt;The consensus is quiet because it doesn't need to be loud. It's not replacing anything overnight. It's just that every year, more teams try process-first design, find that it works, and don't go back.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The convergence described here is formalized in &lt;a href="https://pragmaticalabs.io/jbct.html" rel="noopener noreferrer"&gt;JBCT&lt;/a&gt; (Java Backend Coding Technology) — a methodology with patterns, tooling, and implementation guidance. The approach has been validated against a &lt;a href="https://pragmaticalabs.io/aether.html" rel="noopener noreferrer"&gt;326,000-line distributed runtime&lt;/a&gt; built entirely with process-first design.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Further reading: Scott Wlaschin, &lt;a href="https://pragprog.com/titles/swdddf/domain-modeling-made-functional/" rel="noopener noreferrer"&gt;Domain Modeling Made Functional&lt;/a&gt;. Debasish Ghosh, &lt;a href="https://www.manning.com/books/functional-and-reactive-domain-modeling/" rel="noopener noreferrer"&gt;Functional and Reactive Domain Modeling&lt;/a&gt;. Rico Fritzsche, &lt;a href="https://levelup.gitconnected.com/how-to-model-domain-logic-without-shared-entities-05c938eee73f" rel="noopener noreferrer"&gt;How to Model Domain Logic Without Shared Entities&lt;/a&gt;. Roman Weis, &lt;a href="https://medium.com/codex/lets-build-business-software-an-alternative-approach-to-the-standard-ddd-implementation-47e586b5f81f" rel="noopener noreferrer"&gt;Alternative Approach to DDD&lt;/a&gt;. Sandro Mancuso, &lt;a href="https://www.codurance.com/publications/2017/12/08/introducing-idd" rel="noopener noreferrer"&gt;Interaction-Driven Design&lt;/a&gt;. Jimmy Bogard, &lt;a href="https://www.jimmybogard.com/vertical-slice-architecture/" rel="noopener noreferrer"&gt;Vertical Slice Architecture&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>softwaredesign</category>
      <category>architecture</category>
      <category>domaindrivendesign</category>
      <category>functional</category>
    </item>
    <item>
      <title>We Should Write Java Code Differently: Less Language, More Business</title>
      <dc:creator>Sergiy Yevtushenko</dc:creator>
      <pubDate>Mon, 06 Apr 2026 12:21:55 +0000</pubDate>
      <link>https://dev.to/siy/we-should-write-java-code-differently-less-language-more-business-5b4h</link>
      <guid>https://dev.to/siy/we-should-write-java-code-differently-less-language-more-business-5b4h</guid>
      <description>&lt;p&gt;How much of your code is actually about your business?&lt;/p&gt;

&lt;p&gt;Open any Java service method. Count the lines. How many describe what the business does? And how many are null checks, exception handling, try-catch blocks, type conversions, logging boilerplate, and framework annotations?&lt;/p&gt;

&lt;p&gt;In most codebases, the answer is uncomfortable. Technical ceremony dominates. Business logic hides between the scaffolding. A new developer reads the code and understands &lt;em&gt;how&lt;/em&gt; it works — but not &lt;em&gt;what&lt;/em&gt; it does or &lt;em&gt;why&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;This isn't a skill problem. It's a language problem. Java gives us powerful tools, but doesn't guide us toward using them in ways that preserve business meaning.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Ratio
&lt;/h3&gt;

&lt;p&gt;Consider a typical service method that processes an order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Validate the input (null checks, field validation, exception wrapping)&lt;/li&gt;
&lt;li&gt;Check inventory (try-catch around HTTP call, retry logic, timeout handling, response parsing)&lt;/li&gt;
&lt;li&gt;Calculate pricing (more HTTP, more try-catch, more parsing)&lt;/li&gt;
&lt;li&gt;Create the order (database call, transaction management, exception handling)&lt;/li&gt;
&lt;li&gt;Return the result (response mapping, error conversion)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Five business steps. But the code for each step is 80% technical handling and 20% business intent. The ratio is inverted — the scaffolding is louder than the signal.&lt;/p&gt;

&lt;h3&gt;
  
  
  Shrinking the Technical Part
&lt;/h3&gt;

&lt;p&gt;What if the technical surface was standardized to the point where it almost disappeared?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Return types encode behavior.&lt;/strong&gt; A method returning &lt;code&gt;Result&amp;lt;Order&amp;gt;&lt;/code&gt; tells you it can fail — without looking at the implementation. &lt;code&gt;Option&amp;lt;User&amp;gt;&lt;/code&gt; tells you the value might be absent. &lt;code&gt;Promise&amp;lt;Response&amp;gt;&lt;/code&gt; tells you it's asynchronous. The type signature &lt;em&gt;is&lt;/em&gt; the high-level documentation. No Javadoc needed to explain "this method might throw" — the return type already said it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Composition operators have fixed semantics.&lt;/strong&gt; &lt;code&gt;flatMap&lt;/code&gt; means "if the previous step succeeded, do this next." &lt;code&gt;all()&lt;/code&gt; means "these operations are independent — they have no ordering dependency and can execute in parallel." These aren't just API methods. They're business-level statements about relationships between operations. When you read &lt;code&gt;all(checkInventory, calculatePricing)&lt;/code&gt;, you know these two things don't depend on each other. That's domain knowledge encoded in structure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error types are exhaustive.&lt;/strong&gt; A sealed interface listing every failure mode means a business analyst can read the error hierarchy and understand what can go wrong — without reading implementation code. The errors aren't strings or exception classes buried in catch blocks. They're first-class types that enumerate the business failure domain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Patterns are structural, not creative.&lt;/strong&gt; A Sequencer means "do these things in this order." Fork-Join means "do these things in parallel, combine results." A Leaf is a single operation with no sub-steps. The developer doesn't invent control flow — they select from a small set of patterns that map directly to how business processes work.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Remains
&lt;/h3&gt;

&lt;p&gt;When the technical part shrinks, what's left is business logic — and it becomes the dominant signal in the code.&lt;/p&gt;

&lt;p&gt;The order processing method becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;OrderService&lt;/span&gt; &lt;span class="nf"&gt;orderService&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;InventoryService&lt;/span&gt; &lt;span class="n"&gt;inventory&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                                 &lt;span class="nc"&gt;PricingService&lt;/span&gt; &lt;span class="n"&gt;pricing&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;inventory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;check&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
                               &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;flatMap&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;pricing:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;calculate&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                               &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;OrderResult:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;placed&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three lines. Each line is a business step. The technical ceremony — HTTP calls, serialization, retry, error handling — exists, but it's handled by the runtime and the type system, not by the developer in this method.&lt;/p&gt;

&lt;p&gt;Read it aloud: "Check inventory. Then calculate pricing. Then create the order." That's not a description of the code. That &lt;em&gt;is&lt;/em&gt; the code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Preserving Knowledge
&lt;/h3&gt;

&lt;p&gt;This matters beyond aesthetics.&lt;/p&gt;

&lt;p&gt;When technical ceremony dominates, a new team member reads the code and learns the framework. They understand &lt;em&gt;how&lt;/em&gt; things are wired — which annotations trigger what, which configuration goes where, which exception handler catches what. This knowledge is framework-specific and doesn't transfer.&lt;/p&gt;

&lt;p&gt;When business logic dominates, the same team member reads the code and learns the domain. They understand &lt;em&gt;what&lt;/em&gt; the system does — which operations depend on each other, what can fail, what the valid states are. This knowledge survives framework migrations, team changes, and technology shifts.&lt;/p&gt;

&lt;p&gt;The original developer's intent — the business reasoning behind the code — is preserved in the structure itself. Not in comments that drift from reality. Not in documentation that nobody updates. In the code.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Language Shrinks
&lt;/h3&gt;

&lt;p&gt;Something unexpected happens when you measure language features against business value: most of them become unnecessary.&lt;/p&gt;

&lt;p&gt;Java is a large language. Inheritance hierarchies, checked exceptions, mutable state, reflection, annotation processing, type erasure workarounds, synchronized blocks, volatile fields — these are powerful tools. But when the goal is expressing business logic clearly, how many of them do you actually use?&lt;/p&gt;

&lt;p&gt;The answer is surprisingly few. Records for data. Sealed interfaces for type-safe alternatives. Lambdas and method references for composition. Pattern matching for dispatch. That's most of it.&lt;/p&gt;

&lt;p&gt;The rest — the features that generate conference talks and blog posts about clever techniques — serves the technical ceremony, not the business logic. Class inheritance exists to share implementation, not to model business concepts. Checked exceptions exist to force handling, but &lt;code&gt;Result&lt;/code&gt; types handle errors more expressively. Mutable state exists for performance optimization, but immutable records are sufficient for business data.&lt;/p&gt;

&lt;p&gt;This isn't a limitation. It's a feature. When the useful subset of the language is small:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The learning curve compresses. A new developer doesn't need to master all of Java — just the subset that carries business meaning.&lt;/li&gt;
&lt;li&gt;Code becomes predictable. When there are three ways to express something, developers argue about style. When there's one way, they focus on the domain.&lt;/li&gt;
&lt;li&gt;The "which feature should I use here?" decision disappears. The answer is always the same small set of constructs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The implication is broader than one language. If Java's business-relevant subset is this small, adding more language features doesn't increase business expressiveness — it increases the technical surface that competes for attention with the business logic. Expressiveness comes from domain modeling, not from language syntax.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Boundary Is Clear
&lt;/h3&gt;

&lt;p&gt;The technical part of the code should be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Small&lt;/strong&gt; — a handful of types and patterns, not a framework vocabulary&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Standardized&lt;/strong&gt; — the same patterns everywhere, no per-developer style&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Semantically meaningful&lt;/strong&gt; — each construct maps to a business concept&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The business part should be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dominant&lt;/strong&gt; — more visible than the technical scaffolding&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Readable&lt;/strong&gt; — a sequence of named operations, not a tangle of callbacks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exhaustive&lt;/strong&gt; — every failure mode visible, every dependency declared&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When these two conditions are met, code becomes what it should have been from the start: an executable specification of what the business does.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is the sixth article in the "We Should Write Java Code Differently" series. Previous: &lt;a href="https://dev.to/siy/we-should-write-java-code-differently-the-di-confusion-192h"&gt;The DI Confusion&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>java</category>
      <category>architecture</category>
      <category>cleancode</category>
      <category>backend</category>
    </item>
    <item>
      <title>We Should Write Java Code Differently: The DI Confusion</title>
      <dc:creator>Sergiy Yevtushenko</dc:creator>
      <pubDate>Sun, 05 Apr 2026 08:44:15 +0000</pubDate>
      <link>https://dev.to/siy/we-should-write-java-code-differently-the-di-confusion-192h</link>
      <guid>https://dev.to/siy/we-should-write-java-code-differently-the-di-confusion-192h</guid>
      <description>&lt;p&gt;Inversion of Control is one of the most impactful ideas in software engineering. It fundamentally changed how we structure applications — making code testable, modular, and composable. Dependency Injection, its most common implementation, became the backbone of virtually every modern Java framework.&lt;/p&gt;

&lt;p&gt;But somewhere along the way, DI acquired a second job. And that second job is quietly causing problems.&lt;/p&gt;

&lt;h3&gt;
  
  
  Assembly vs. Provisioning
&lt;/h3&gt;

&lt;p&gt;DI was designed to solve one problem: &lt;strong&gt;assembling an application from its components.&lt;/strong&gt; Controller depends on service, service depends on repository — wire them together, done. The components are internal. The wiring is deterministic. Configuration is minimal or zero.&lt;/p&gt;

&lt;p&gt;But most frameworks also use DI for something fundamentally different: &lt;strong&gt;resource provisioning.&lt;/strong&gt; Database connections, HTTP clients, message brokers, caches, stream consumers — external resources that the application needs access to.&lt;/p&gt;

&lt;p&gt;These two concerns look similar on the surface. Both involve "giving a component something it needs." But they behave completely differently:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Assembly&lt;/strong&gt; — internal components, part of the application. Minimal or no configuration. Created once at startup. Dependencies are your code. No security concerns. Failures are deterministic, caught at compile time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Provisioning&lt;/strong&gt; — external resources, part of the infrastructure. Environment-dependent, complex configuration. Managed lifecycle: pools, reconnects, health checks. Dependencies are drivers, SDKs, adapters. Requires credentials, certificates, rotation. Failures are environmental, discovered at runtime.&lt;/p&gt;

&lt;p&gt;By fusing these into one mechanism, frameworks created a set of consequences that the industry accepted as normal.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Consequences
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Your application bundles infrastructure.&lt;/strong&gt;&lt;br&gt;
Database drivers, connection pools, HTTP client libraries, messaging SDKs — they all live in your application's dependency tree. A typical backend service is 60% infrastructure dependencies, 40% business logic. Your &lt;code&gt;pom.xml&lt;/code&gt; is a manifest of things that aren't your problem but became your problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Environment leaks into the artifact.&lt;/strong&gt;&lt;br&gt;
Dev, staging, production — each needs different resource configurations. Connection strings, pool sizes, timeouts, retry policies. The application artifact is identical, but the resource configuration isn't. Managing these variations across services and environments becomes a combinatorial challenge that grows with every new service and every new resource.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Credentials live where they shouldn't.&lt;/strong&gt;&lt;br&gt;
Because the application provisions its own resources, it needs secrets. Database passwords, API keys, certificates. Every service, every environment. Now multiply by the number of services. Secret management becomes an infrastructure project of its own — not because the problem is inherently hard, but because every application was given responsibility for its own credentials.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Infrastructure changes become application changes.&lt;/strong&gt;&lt;br&gt;
Upgrading a connection pool library? Touch every service. Switching to a different database driver? Rebuild and redeploy every service that uses a database. A security CVE in an adapter library requires coordinated redeployment across the fleet. Infrastructure concerns create application-level change propagation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Testing fights the wrong battle.&lt;/strong&gt;&lt;br&gt;
A significant portion of test setup is dedicated to mocking or configuring infrastructure that isn't the application's concern. Integration tests need containers for databases, message brokers, caches. The test is verifying business logic, but the setup is provisioning resources.&lt;/p&gt;

&lt;h3&gt;
  
  
  Separating the Concerns
&lt;/h3&gt;

&lt;p&gt;What if assembly and provisioning were handled by different mechanisms, at different layers?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Assembly&lt;/strong&gt; — connecting internal components — can be fully automated. If a use case depends on a repository, and both are part of the application, the wiring is deterministic. No configuration file needed. Annotation processing can resolve it at compile time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Provisioning&lt;/strong&gt; — providing access to external resources — belongs to the runtime environment, not the application. The application declares &lt;em&gt;what&lt;/em&gt; it needs: "I need a SQL connection," "I need a notification channel," "I need a stream." The runtime decides &lt;em&gt;how&lt;/em&gt; to provide it based on the deployment environment.&lt;/p&gt;

&lt;p&gt;This separation has immediate consequences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;pom.xml&lt;/code&gt; contains only business dependencies. Infrastructure dependencies belong to the runtime.&lt;/li&gt;
&lt;li&gt;Same artifact deploys to any environment without configuration changes.&lt;/li&gt;
&lt;li&gt;Credentials never touch the application. The runtime handles authentication with the infrastructure.&lt;/li&gt;
&lt;li&gt;A security patch in a database driver is a runtime update — zero application rebuilds, zero redeployments.&lt;/li&gt;
&lt;li&gt;Tests focus on business logic. No mock infrastructure, no test containers for resources the application doesn't own.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The application becomes what it should have been from the start: pure business logic with declared resource requirements. Everything between the business logic and production is &lt;a href="https://pragmaticalabs.io/docs/developer-guide.html#resource-provisioning" rel="noopener noreferrer"&gt;the runtime's responsibility&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Boundary
&lt;/h3&gt;

&lt;p&gt;This isn't an argument against DI. Inversion of Control remains one of the best ideas our field has produced. The argument is against overloading DI with a concern it wasn't designed for — and paying the price in dependency sprawl, configuration complexity, and operational coupling.&lt;/p&gt;

&lt;p&gt;The boundary is clear: if it's your code, assemble it. If it's infrastructure, declare it and let the environment provide it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is the fifth article in the "We Should Write Java Code Differently" series. Previous: &lt;a href="https://dev.to/siy/we-should-write-java-code-differently-frictionless-prod-3mg8"&gt;Frictionless Prod&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>java</category>
      <category>architecture</category>
      <category>webdev</category>
      <category>backend</category>
    </item>
  </channel>
</rss>
