From Requirements to Code in Eight Questions
Every backend system is a collection of processes, not a collection of entities.
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.
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 does, not what it stores. Six of them are documented in The Quiet Consensus, but the pattern extends far beyond those six.
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.
This article presents Java Backend Design Technology (JBDT) — the design phase of Java Backend Coding Technology (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.
For any feature, ask these eight questions. The answers produce the code structure mechanically.
That’s the entire design process. No other questions need answering. Pattern selection, type placement, error strategy — all determined by these eight answers.
Let’s see it work.
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.
Question 1 — What triggers this? A customer submits an order.
Question 2 — What data does it need? Customer ID, list of items with quantities, shipping address, payment method.
record Request(String customerId, List<OrderItem> items, String shippingAddress, String paymentMethod) {} record OrderItem(String productId, int quantity) {}
Question 3 — What does success look like? An order confirmation with an ID and estimated delivery.
record Response(String orderId, LocalDate estimatedDelivery) {}
Question 4 — What can go wrong? Invalid inputs, insufficient inventory, payment declined.
sealed interface PlaceOrderError extends Cause { enum General implements PlaceOrderError { EMPTY_CART("Cart is empty"), INVALID_ADDRESS("Shipping address is invalid"); // ... } record InsufficientInventory(String productId, int requested, int available) implements PlaceOrderError { /* ... */ } record PaymentDeclined(String reason) implements PlaceOrderError { /* ... */ } }
Question 5 — What are the steps? Validate the request. Check inventory. Process payment. Create order. Send confirmation.
Question 6 — Which steps depend on each other? 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.
This directly produces the composition:
static PlaceOrder placeOrder(CheckInventory checkInventory, ProcessPayment processPayment, CreateOrder createOrder, SendConfirmation sendConfirmation) { return request -> ValidRequest.validRequest(request) .async() .flatMap(valid -> reserveOrder(checkInventory, processPayment, valid)) .flatMap(createOrder::apply) .flatMap(sendConfirmation::apply); } private static Promise<ReservedOrder> reserveOrder(CheckInventory checkInventory, ProcessPayment processPayment, ValidRequest valid) { return Promise.all(checkInventory.apply(valid), processPayment.apply(valid)) .map(ReservedOrder::new); }
Questions 7 and 8 — no conditional paths or collection processing in the main flow.
Notice what happened. We didn’t draw a class diagram. We didn’t debate which aggregate owns what. We didn’t create a shared Order entity used by every feature. We asked eight questions, wrote down the answers, and the code structure emerged.
Notice also: ValidateCart, ProcessPayment, InsufficientInventory, PaymentDeclined — 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.
To show this isn’t specific to e-commerce, let’s design a content publishing feature.
Requirements: an author submits an article. The system validates the content, checks for duplicates, generates a slug, and publishes to multiple platforms.
Walking through the questions quickly:
static PublishArticle publishArticle(ValidateContent validate, CheckDuplicates checkDups, GenerateSlug generateSlug, PlatformPublisher publisher) { return request -> validateArticle(validate, checkDups, request) .flatMap(generateSlug::apply) .flatMap(article -> publisher.publishAll(article, request.platforms())); } private static Promise<ValidArticle> validateArticle(ValidateContent validate, CheckDuplicates checkDups, Request request) { return Promise.all(validate.apply(request), checkDups.apply(request)) .map(ValidArticle::new); }
Different domain, same eight questions, same mechanical process. The structure is determined by the answers, not by preferences.
There’s a deeper structure underneath the eight questions.
Every backend process is fundamentally an act of knowledge gathering. Each step acquires a piece of knowledge. The process ends — successfully or not — when enough knowledge has accumulated to formulate an answer.
In PlaceOrder:
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.
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.
The knowledge-gathering view has a formal structure. Three operators describe how pieces of knowledge relate:
Between operators, transformation functions convert one piece of knowledge into another — pure business logic.
A lending system needs to make a credit decision. The process:
As a data dependency graph:
CreditDecision = ApplyPolicy(
Assess(
ALL(
ANY(InternalScore, ExternalBureau),
PaymentHistory
)
)
)
Reading from the inside out:
ANY(InternalScore, ExternalBureau) — gather credit score from whichever source responds successfully first, any response is equally correct.ALL(..., PaymentHistory) — gather the credit score (from either source) AND the payment history independently, in parallel.Assess(...) — transform both pieces of knowledge into a risk assessment. Pure function, no I/O.ApplyPolicy(...) — transform the risk assessment into a lending decision. Pure function.This maps directly to code:
static ResolveCredit resolveCredit(InternalScoring internal, ExternalBureau external, PaymentHistoryService history, RiskAssessor assessor, LendingPolicy policy) { return request -> gatherCreditData(internal, external, history, request) .map(assessor::assess) .map(policy::apply); } private static Promise<CreditData> gatherCreditData(InternalScoring internal, ExternalBureau external, PaymentHistoryService history, Request request) { return Promise.all(obtainCreditScore(internal, external, request), history.retrieve(request.customerId())) .map(CreditData::new); } private static Promise<CreditScore> obtainCreditScore(InternalScoring internal, ExternalBureau external, Request request) { return internal.score(request) .orElse(() -> external.score(request)); }
Three methods, one pattern each. resolveCredit is the Sequencer (gather → assess → decide). gatherCreditData is the Fork-Join — the ALL operator gathering independent pieces of knowledge in parallel. obtainCreditScore is the fallback — the ANY operator trying the internal source first, falling back to external. The sequential .map calls are transformations that convert gathered knowledge into the final answer.
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.
The product owner says: “We need to check for fraud before processing payment.”
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?
In process-first design, the response is mechanical:
CheckFraudstatic PlaceOrder placeOrder(CheckInventory checkInventory, ProcessPayment processPayment, CreateOrder createOrder, SendConfirmation sendConfirmation, CheckFraud checkFraud) { return request -> ValidRequest.validRequest(request) .async() .flatMap(valid -> reserveOrder(checkInventory, processPayment, checkFraud, valid)) .flatMap(createOrder::apply) .flatMap(sendConfirmation::apply); } private static Promise<ReservedOrder> reserveOrder(CheckInventory checkInventory, ProcessPayment processPayment, CheckFraud checkFraud, ValidRequest valid) { return Promise.all(checkInventory.apply(valid), verifyAndPay(checkFraud, processPayment, valid)) .map(ReservedOrder::new); } private static Promise<PaymentResult> verifyAndPay(CheckFraud checkFraud, ProcessPayment processPayment, ValidRequest valid) { return checkFraud.apply(valid) .flatMap(_ -> processPayment.apply(valid)); }
One new step interface. One change to the composition. No restructuring. No entity changes. No architecture discussion.
This is what mechanical design evolution looks like: patterns are used from the start, so evolution is adding and recomposing — never restructuring.
“But what about the database? Don’t you end up with entities anyway?”
Yes — and that’s fine. The difference is how they get there.
Multiple use cases converge on the same database tables. PlaceOrder writes order rows. CancelOrder updates the status column. TrackOrder reads shipping info. GenerateInvoice reads billing fields. The orders table is the union of what these processes need.
But this entity emerged 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.
The key insight: entities are discovered, not invented. They’re the natural intersection of processes that share persistence — a composition of views, not a universal model.
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 Beyond Aggregates). 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.
Design meetings get shorter. 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.
The six JBCT structural patterns map directly to BPMN constructs — code written using these patterns is structurally equivalent to a business process diagram. For the full pattern-BPMN mapping, see the JBCT series.
Testing becomes a side effect of design. Every step interface is a test seam. Stub it, test the composition. The design produces testable code by construction — no special effort required.
AI-assisted development benefits directly. 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.
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.
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.
That’s JBDT in practice.
Java Backend Design Technology is the design phase of Java Backend Coding Technology — a complete methodology covering design, implementation patterns, testing strategy, and tooling. JBDT has been validated against a 326,000-line distributed runtime (Aether) and formalized into a comprehensive learning series.
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.
Further reading: