DEV Community

Sergiy Yevtushenko
Sergiy Yevtushenko

Posted on

When Types Become the Business Language

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.

Two traditions have tried to close this gap. Both got partway. Neither finished.

What Object-Oriented Design Promised

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 Order, Customer, Invoice -- names pulled directly from the business -- and find business logic inside each class.

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.

Entities, it turned out, are the wrong unit. A Seat in a booking flow is a row and a seat number. A Seat in availability checking is an occupancy flag. A Seat 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.

The business doesn't think in entities first. It thinks in processes: place an order, confirm availability, charge a card. 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.

What Pure Functional Programming Offered

Functional programming took a different route. Instead of modeling the domain with classes, it modeled computations 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.

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.

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

Two Halves of the Same Idea

OO wanted types to carry business meaning but stopped at the nominal level: Customer 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.

What if types carried business 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?

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.

Java Backend Coding Technology (JBCT) is one way of writing this middle ground down.

Four Shapes, One Principle

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

Three containers appear only when the computation genuinely has the modality:

  • Option<T> -- the value may be missing. "The customer may not exist."
  • Result<T> -- the computation may fail. "Placing the order might fail."
  • Promise<T> -- the value may be deferred. "Fulfillment happens later."

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.

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

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.

Six Patterns, One Vocabulary

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:

  • Leaf -- 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)
  • Sequencer -- a sequence of steps where each depends on the previous
  • Fork-Join -- independent steps run together, combined when all complete
  • Condition -- branching based on a decision
  • Iteration -- the same step applied to many inputs
  • Aspects -- observational wrapping (logging, tracing, metrics) around another step

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

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.

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.

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.

Leaves and the Quarantine Principle

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.

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 business needs, not what the technology provides. Not PostgresUserDao. Not HttpOrderClient. UserRepository. OrderPlacement. The interface is a domain statement; the implementation behind it is an implementation.

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.

Three containers, six patterns, one boundary primitive. The whole vocabulary fits on a napkin and carries enterprise complexity.

The Convergence

This approach did not arrive fully formed. Practitioners in different ecosystems -- working independently, without coordination -- have been arriving at shapes of it for years. Six of them, across F#, Rust, TypeScript, Scala, C#, and Java, describe the same structural adaptation with different vocabulary. 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.

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.

The Compiler as Participant

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.

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 Result<Customer> lookup(CustomerId id) 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.

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.


JBCT is documented at pragmatica.dev.

Top comments (0)