Designing the System

From Requirements to Technical Design

LuettavaEsikatselu

Katselet ilmaista esikatseluoppituntia.

In Module 1, we did something important before writing production code.

We understood:

  • what the business wants
  • what users need to be able to do
  • which behaviours are required
  • which behaviours are out of scope
  • which engineering constraints already exist
  • when a change can be considered Done
  • what the initial engineering backlog looks like

We now have enough context about the problem.

The next question is:

How should we design the system so that it satisfies those requirements?

This is where Technical Design begins.

The goal of technical design is not to create impressive diagrams or introduce as many architecture patterns as possible.

Its purpose is to make important technical decisions clear enough before implementation so that the team can build one coherent system.

In this lesson, we will learn how to move from requirements into technical design.

We will not finalize the database schema, package structure, or Java classes yet.

First, we need to understand which questions the design must answer.


Requirements Tell Us What; Design Decides How

We have a requirement:

Customers can create an Order containing
one or more available Products.

We also know:

Inventory must be sufficient.

The backend determines prices.

Order creation is all-or-nothing.

Purchase-time prices must be preserved.

These requirements tell us what behaviour the system must provide.

They do not tell us:

Which application components should exist?

Where should Order creation logic live?

How should Product and Inventory interact?

What should be persisted in PostgreSQL?

What behaviour belongs inside Order?

Where should transaction boundaries exist?

How should external Payment logic connect
to Order workflows?

Those are technical-design questions.

Conceptually:

Requirement
    ↓
What must happen?

and:

Technical Design
    ↓
How should our system make it happen?

Technical Design Is More Than an Architecture Diagram

When engineers hear technical design, they often think about boxes and arrows.

For example:

Handler
   ↓
UseCase
   ↓
Repository
   ↓
Database

That may be part of the architecture.

But useful technical design answers deeper questions:

What are the core Domain concepts?

Who owns each responsibility?

Which rules must always remain true?

Where do transactions begin and end?

Which data must be persisted?

Which data must be historical snapshots?

Which system boundaries exist?

How are failures handled?

Which decisions are difficult to reverse?

Which alternatives did we consider?

What are the known risks?

Drawing boxes is easy.

Assigning responsibilities correctly is harder.


Start With the Problem, Not the Framework

A weak starting point would be:

We use Spring Boot.

Therefore we need:

Controller
Service
Repository
Entity
DTO
Mapper

This begins with framework structure rather than the problem.

A better sequence is:

What business behaviour are we implementing?

Then:

What responsibilities are required?

Then:

How should those responsibilities interact?

And only after that:

How do we express the design using
Java, Spring Boot, PostgreSQL,
and our engineering context?

The framework should support the design.

The design should not exist merely to resemble a Spring Boot tutorial.


Our Design Inputs

Technical design does not start from a blank page.

We already have several important inputs.

Product Requirements

Customers browse available Products.

Customers place Orders.

Customers view Order history.

Customers cancel eligible Orders.

Customers pay through an external Payment provider.

Administrators manage Products and Inventory.

Acceptance Criteria

For Order creation:

An Order contains at least one item.

Referenced Products must exist.

Products must be orderable.

Quantities must be positive.

Inventory must be sufficient.

The backend determines authoritative prices.

Purchase-time prices remain historical.

Any invalid item causes the whole operation to fail.

Engineering Context

Java

Spring Boot

Gradle

REST / JSON

PostgreSQL

Flyway

Existing identity capability

External Payment provider

Containerized deployment

Non-Functional Concerns

security

data integrity

reliability

maintainability

observability

A good technical design has to satisfy all of these together.


Good Design Is Constraint Satisfaction

A useful way to think about design is:

Requirements
+
Constraints
+
Risks
+
Trade-offs
        ↓
Technical Design

Consider:

Order creation must be all-or-nothing.

Our engineering context says:

Orders and Inventory belong to the same
PostgreSQL-backed application.

That gives us a design clue:

A local database transaction
is likely an appropriate mechanism.

We are deriving the solution from the problem.

We are not saying:

Transactions are useful,
so let's find somewhere to use them.

Design Decisions Need Reasons

Suppose we decide:

OrderItem stores its purchase-time unit price.

Why?

Because:

Historical Order prices must not change
when the Product's current price changes.

The reasoning is traceable:

Requirement
    ↓
Historical pricing must remain stable
    ↓
OrderItem must preserve
purchase-time unitPriceCents

This kind of reasoning later belongs in design documentation.


Identify the Core UseCases First

Before creating classes, tables, or packages, identify the workflows the backend must support.

Our major v1 UseCases include:

Create Product

Update Product

Deactivate Product

Browse Products

Manage Inventory

Create Order

View Order History

View Order Detail

Cancel Order

Pay Order

These UseCases expose application responsibilities more clearly than framework components do.


Example: Create Order UseCase

From our Acceptance Criteria, Create Order conceptually needs to:

Receive requested items
        ↓
Identify the Customer
        ↓
Validate the request
        ↓
Load referenced Products
        ↓
Ensure Products are active
        ↓
Validate quantities
        ↓
Consume available Inventory safely
        ↓
Read authoritative Product prices
        ↓
Capture purchase-time prices
        ↓
Create the Order
        ↓
Persist Order and OrderItems

This is not implementation yet.

It is a behavioural workflow.

Now we can ask:

Which responsibility belongs where?


Identify Responsibilities Before Classes

From the workflow, we can identify responsibilities such as:

Represent an Order.

Represent an OrderItem.

Determine whether an Order can be cancelled.

Determine whether an Order can be paid.

Calculate an Order total.

Retrieve Product information.

Consume Inventory safely.

Restore Inventory after cancellation.

Persist Orders.

Coordinate Order creation.

Later, those responsibilities may become:

Domain objects

UseCases

Repositories

Handlers

external Services

But responsibility comes before class naming.


Responsibilities Are More Stable Than Class Names

Suppose we initially imagine:

OrderService

Later, we decide the system is clearer with:

CreateOrderUseCase

CancelOrderUseCase

PayOrderUseCase

The class structure changes.

The underlying responsibilities remain:

coordinate Order creation

apply Product and Inventory rules

persist resulting state

Design discussions should focus on responsibilities first.


UseCase Is Our Workflow Vocabulary

For this course, internal application workflows use:

UseCase

Examples:

CreateProductUseCase

UpdateProductUseCase

SetInventoryUseCase

CreateOrderUseCase

GetOrderHistoryUseCase

CancelOrderUseCase

PayOrderUseCase

We reserve:

Service

for external or third-party integrations.

For example:

PaymentService

This distinction keeps internal orchestration separate from external integration concerns.


Start With Domain Concepts

The requirements repeatedly refer to concepts such as:

Product

Inventory

Order

OrderItem

Payment

Customer identity

These are useful Domain concepts.

But not every noun automatically becomes:

an Entity

a database table

a Java class

We need to reason about identity, lifecycle, ownership, and behaviour.

For example, Customer identity matters to Orders.

That does not mean we need a local Customer Entity.

Our application uses:

CustomerId

as authenticated application context.


Domain Model vs Database Model

A common mistake is beginning design with tables.

For example:

products

inventory

orders

order_items

payments

Some may eventually exist.

But a database schema is only one representation of the Domain.

A better sequence is:

Business concepts
        ↓
Relationships and rules
        ↓
Domain model
        ↓
Persistence model
        ↓
Database schema

If we start with tables, we may model storage before behaviour.


Order Is More Than a Database Row

If we think only in persistence terms:

orders

id
customer_id
status
created_at

we may end up with code like:

order.setStatus(
        OrderStatus.CANCELLED
);

from anywhere.

But cancellation has rules:

Only UNPAID Orders can be cancelled.

PAID Orders cannot be cancelled in v1.

CANCELLED Orders cannot be cancelled again.

So Order is not simply persisted data.

It has behaviour and invariants.


Identify Invariants

Technical design should identify rules that must remain true.

Important examples:

An Order contains at least one OrderItem.
OrderItem quantity is positive.
OrderItem unitPriceCents is non-negative.
Successful Order creation must not consume
more Inventory than is available.
Order total equals the sum
of OrderItem totals.
A CANCELLED Order cannot be paid.
A PAID Order cannot be cancelled in v1.
A Customer cannot operate on
another Customer's Order.

These are core design inputs.


Different Invariants Need Different Enforcement

Consider:

quantity must be positive

We may enforce this at:

HTTP request validation

and when constructing Domain state.

Now consider:

Customer may access only their own Order.

This depends on authenticated identity and belongs to:

security + application access boundaries

Now consider:

Inventory must not be oversold
under concurrent Order creation.

That needs persistence-level concurrency reasoning.

There is no universal rule like:

all validation goes in the Handler

or:

all business rules go in one Service

Different rules need different boundaries.


Identify Transactional Workflows

Some operations modify several pieces of local state together.

Order creation involves:

consume Inventory
+
create Order
+
persist OrderItems

Cancellation involves:

transition Order to CANCELLED
+
restore consumed Inventory

Both have atomicity requirements.

From our Acceptance Criteria:

Order creation is all-or-nothing.

So technical design needs a transaction boundary.

Conceptually:

BEGIN

    validate current state

    consume Inventory

    persist Order

    persist OrderItems

COMMIT

If something fails:

ROLLBACK

The exact implementation comes later.


Cancellation Is Also Transactional

Cancellation is not simply:

order.cancel();

The workflow includes:

load Customer-owned Order

validate current state

transition Order to CANCELLED

restore consumed Inventory

persist changes

These state changes must succeed or fail together.

Otherwise we could get:

Order = CANCELLED

Inventory = not restored

which is inconsistent.


Payment Is Different

Payment crosses an external system boundary.

Conceptually:

PostgreSQL
    +
External Payment Provider

A PostgreSQL transaction cannot roll back a successful provider-side Payment.

Therefore:

@Transactional

cannot make the complete Payment workflow atomic across both systems.

That gives us an important design principle:

External side effects require different reasoning from local database mutations.

We will design this carefully in the external-services module.


System Boundaries Matter

Our engineering context established:

Existing Identity Capability
        ↓
Order Management Backend
        ↓
Payment Provider

and:

Order Management Backend
        ↓
PostgreSQL

The Order Management Backend should not:

store user passwords

implement account recovery

own identity lifecycle

perform card-network processing

Those responsibilities belong elsewhere.

Clear boundaries protect scope.


Internal Boundaries Matter Too

One deployable application still needs logical separation.

Conceptually:

Product

Inventory

Order

Payment

Security / Customer Identity Context

These are not microservices.

They are logical responsibility boundaries inside one application.

Our goal is:

one deployable application

without:

one giant undifferentiated codebase

Modular Thinking Without Microservices

A useful conceptual view:

Order Management Backend

├── Product
├── Inventory
├── Order
├── Payment
└── Security / Identity Context

Product

Product identity

name

current price

active/inactive state

Inventory

available quantity

administrative quantity setting

Order-time consumption

cancellation restoration

Order

Order lifecycle

OrderItems

historical unit prices

derived total

Customer ownership

cancellation rules

Payment

Order Payment workflow

external provider boundary

Security / Identity Context

authenticated identity

CustomerId resolution

Administrator authorization

These are code-organization boundaries, not deployment boundaries.


Avoid Premature Layer Explosion

A Java application can quickly become:

controller

facade

application service

domain service

manager

handler

processor

repository

DAO

mapper

adapter

gateway

for a simple feature.

More layers do not automatically mean better architecture.

Each abstraction should solve a real problem.

Our question is:

What is the simplest structure that keeps responsibilities understandable and protects required behaviour?


Avoid One Giant Service Too

The opposite extreme is:

public class CommerceService {

    createProduct();

    updateInventory();

    createOrder();

    cancelOrder();

    payOrder();

    getOrderHistory();
}

This combines unrelated responsibilities.

As the codebase grows, such a class becomes difficult to:

understand

test

review

change safely

We want:

not unnecessarily fragmented

and:

not unnecessarily centralized

Domain Behaviour vs Workflow Coordination

Consider cancellation.

Some rules belong naturally inside Order:

Can this Order transition to CANCELLED?

Is the current status eligible?

The workflow also needs:

load the Order

enforce Customer ownership

restore Inventory

persist changes

Order should not connect to PostgreSQL or call Repositories.

This gives us:

Domain
    ↓
owns intrinsic business rules

and:

UseCase
    ↓
coordinates Domain,
Repositories,
and external dependencies

Example: Cancel Order

Conceptually:

CancelOrderUseCase
        ↓
load Customer-owned Order
        ↓
ask Order to cancel
        ↓
restore consumed Inventory
        ↓
persist changes

Order knows:

PAID cannot transition to CANCELLED.

The UseCase knows:

which Customer is making the request

and:

which Repositories participate

and:

Inventory restoration belongs
to the same local transaction

Data Ownership Must Be Clear

Ask:

Who owns this fact?

For example:

Current Product price
→ Product
Available quantity
→ Inventory
Purchase-time unit price
→ OrderItem

These values may look related, but they represent different facts.


Current Price vs Historical Price

Suppose:

Product P1

priceCents = 2000

A Customer creates an Order.

The OrderItem records:

Product P1

quantity = 2

unitPriceCents = 2000

Later:

Product P1

priceCents = 2500

The Product owns:

the current catalog price

The OrderItem owns:

the purchase-time historical price

These are different facts.


Duplicate Data Is Not Automatically Wrong

Someone may ask:

Why store price in both Product and OrderItem?

Because:

Product.priceCents
=
What does this Product cost now?

while:

OrderItem.unitPriceCents
=
What did this Product cost
when the Order was created?

This is not accidental duplication.

It represents different historical meaning.


Money Representation Is Already Decided

Our project uses:

Money
→ long cents

For example:

2000
→ 20.00 currency units

and:

8990
→ 89.90 currency units

PostgreSQL uses:

BIGINT

for these values.

We do not use:

double

float

BigDecimal

for authoritative monetary arithmetic in this project.


Identify Relationships

Conceptually:

CustomerId
    1
    │ owns
    ▼
    *
Order
Order
   1
   │ contains
   ▼
   *
OrderItem
OrderItem
   *
   │ references
   ▼
   1
Product
Product
   1
   │ corresponds to
   ▼
   0..1
Inventory

That final relationship matters.

A Product may exist without an Inventory record.

Such a Product is not orderable.


Product and Inventory Have Different Lifecycles

Questions include:

Can Product exist without Inventory?

Can Inventory exist without Product?

Can Inventory still be changed
after Product deactivation?

Does deactivation delete Inventory?

Our direction is:

Product may exist without Inventory.

Inventory cannot exist for
a nonexistent Product.

Inactive Product may still have Inventory.

Deactivation does not delete Inventory.

Product lifecycle and Inventory state are separate concerns.


Deactivation Does Not Rewrite History

If a Product becomes inactive:

future ordering is disabled

but:

historical Orders remain valid

The Product is not physically deleted.

OrderItem already preserves:

ProductId

quantity

unitPriceCents

so historical pricing does not depend on the Product's current price.


Design the Order State Transitions

Our v1 lifecycle contains only:

UNPAID

PAID

CANCELLED

Conceptually:

UNPAID
   │
   ├── confirmed Payment ──▶ PAID
   │
   └── cancellation ───────▶ CANCELLED

And:

PAID
   ──X──▶ CANCELLED

Also:

CANCELLED
   ──X──▶ PAID

These transitions are more useful than inventing a long ecommerce state list.


State-Machine Thinking Without a Framework

We do not need a state-machine library.

But we should ask:

Which states exist?

Which transitions are valid?

What causes each transition?

Which transitions are forbidden?

For v1:

UNPAID → PAID

is valid after confirmed Payment success.

UNPAID → CANCELLED

is valid after cancellation.

PAID → CANCELLED

is unsupported.

CANCELLED → PAID

is invalid.


Do Not Add Future States “Just in Case”

Avoid:

PROCESSING

PACKED

SHIPPED

DELIVERED

RETURNED

REFUNDED

Our current Product does not support fulfilment or refunds.

Every extra state creates:

more transitions

more validation

more tests

more ambiguity

Design what the current scope needs.


API Boundaries Should Follow UseCases

We know the application uses REST/JSON.

Likely resources include:

/products

/inventory

/orders

The UseCases suggest operations such as:

POST /api/v1/orders

for Order creation.

GET /api/v1/orders

for Order history.

GET /api/v1/orders/{orderId}

for Order detail.

POST /api/v1/orders/{orderId}/cancel

for cancellation.

POST /api/v1/orders/{orderId}/pay

for Payment.

These expose meaningful application operations.


Do Not Let Database Shape Leak Into the API

A database table named:

order_items

does not imply a public endpoint:

POST /api/v1/order-items

The user operation is:

Create an Order containing items.

The backend should own that complete workflow.

API design should reflect business operations, not table CRUD.


Avoid Client-Orchestrated Transactions

This would be poor design:

1. GET Product price

2. GET Inventory

3. PATCH Inventory

4. POST Order

5. POST OrderItem

Many things can fail between those requests.

The client may stop after Inventory mutation.

Two clients may race.

The client may use stale pricing.

Instead:

POST /api/v1/orders

represents one backend-owned business workflow.


Design Persistence Around Behaviour

Persistence should support real UseCases.

Examples:

Find Product by ProductId

Find orderable Products

Read Inventory by ProductId

Consume Inventory if available

Restore Inventory

Save Order

Find Order by OrderId

Find Customer-owned Order

List Customer Orders

We should not invent generic Repository methods for hypothetical queries.

Persistence operations should emerge from application needs.


Repository Is an Application Boundary

UseCases should depend on meaningful Repository contracts.

Conceptually:

CreateOrderUseCase
        ↓
ProductRepository

InventoryRepository

OrderRepository

Repository implementations may use JPA or SQL internally.

The UseCase should focus on business workflow rather than persistence mechanics.


Database Constraints Can Reinforce Invariants

Some rules deserve persistence-level protection.

For example:

available_quantity >= 0

can be reinforced with:

CHECK (available_quantity >= 0)

Likewise:

quantity > 0

and:

price_cents >= 0

can be protected at database level.

Database constraints do not replace application validation.

They add another integrity boundary.


Not Every Rule Belongs in the Database

Consider:

A PAID Order cannot be cancelled.

That is a Domain transition rule.

Trying to encode every lifecycle rule purely as database constraints would make the design awkward.

Different invariants belong at different boundaries.


Multiple Layers Can Protect the Same Risk

For quantity:

HTTP validation
        ↓
reject obvious bad input
Domain validation
        ↓
prevent invalid Domain state
Database constraint
        ↓
protect persisted integrity

This is not automatically bad duplication.

Each boundary provides a different kind of protection.


Concurrency Must Be Recognized During Design

Suppose:

available Inventory = 1

Two requests arrive:

Request A wants 1.

Request B wants 1.

A naive implementation could do:

A reads 1.

B reads 1.

A validates.

B validates.

A subtracts 1.

B subtracts 1.

That can violate Inventory correctness.

So:

Do not sell more quantity
than is available.

has a concurrency consequence.


“Check Then Update” Is Not Enough

This alone is unsafe:

if (
        inventory.availableQuantity()
                >= requestedQuantity
) {
    inventory.decrease(
            requestedQuantity
    );
}

The check can become stale before the database write commits.

Our design therefore needs:

concurrency-safe Inventory mutation

at the persistence boundary.

The exact implementation comes later.


Order Creation Is One Atomic Local Operation

Suppose an Order contains:

Product A × 2

Product B × 3

Inventory consumption for Product A succeeds.

Product B then fails because Inventory is insufficient.

The required result is:

No Order is created.

No Inventory remains consumed.

Therefore:

Order creation
=
one transactional unit

Error Handling Is Part of Design

Expected failures include:

Product not found

Product inactive

Insufficient Inventory

Order not found

Order not cancellable

Order not payable

These should not appear to API clients as random Java or persistence exceptions.

Architecture should distinguish:

expected business failure

from:

unexpected system failure

The exact public API error format comes later.


Business Failure vs System Failure

Business failure:

Requested quantity exceeds available Inventory.

The application is behaving correctly.

System failure:

PostgreSQL is unavailable.

or:

Unexpected programming failure.

The distinction affects:

HTTP mapping

logging

metrics

retry behaviour

incident investigation

Payment Needs a Clear Integration Boundary

Without isolation, we might end up with:

PayOrderUseCase
        ↓
provider URL building

HTTP details

provider JSON

provider status codes

credentials

all mixed together.

A cleaner design is:

PayOrderUseCase
        ↓
PaymentService
        ↓
Provider-specific HTTP integration

The UseCase understands:

request Payment

interpret application-level outcome

The external PaymentService understands:

provider URL

HTTP

provider request/response format

provider-specific errors

Do Not Build a Generic Payment Framework

We are not creating:

Stripe adapter

Adyen adapter

PayPal adapter

generic provider plugin framework

without a requirement.

We currently have one provider.

The boundary exists because external integration concerns differ from Order business logic.


Identity Boundary

The identity capability authenticates users.

Our backend receives information such as:

stable authenticated user identifier

roles / permissions

The Domain should not depend on:

JWT parsing

token structure

identity-provider implementation details

For Customer workflows:

authentication
    ↓
CustomerId
    ↓
UseCase

For Administrator operations:

authentication
    ↓
role / permission
    ↓
authorization boundary

Authorization Is More Than an Annotation

Suppose a Customer requests an Order that belongs to another Customer.

Security must block access.

But ownership also matters in application access.

A useful pattern is to scope lookup using:

orderId
+
CustomerId

rather than:

load arbitrary Order
then remember to compare ownership

Authorization is not merely a controller annotation concern.


Read Patterns Matter

The system needs to:

browse available Products

and:

view Customer Order history

These are list operations.

The design should not assume:

load every row

We already know retrieval must be bounded.

Pagination will therefore influence API and persistence design.


Write Patterns Matter Too

Important writes include:

Create Product

Update Product

Set Inventory quantity

Create Order

Cancel Order

Apply successful Payment

Some are simple.

Some are multi-state.

Some cross external boundaries.

For example:

Update Product name

is simple.

Create Order
+
consume Inventory

is transactional.

Pay Order
+
call external provider

is cross-system.

Not all writes should be designed the same way.


Think in Failure Points

For every workflow, ask:

Where can this fail?

Order creation:

invalid request

Product missing

Product inactive

duplicate Product

invalid quantity

insufficient Inventory

database failure

Cancellation:

Order missing

wrong Customer

Order already CANCELLED

Order already PAID

Inventory integrity problem

database failure

Payment:

Order missing

wrong Customer

Order not payable

provider rejection

provider timeout

provider success with local failure

database failure

Failure analysis is part of technical design.


Do Not Prematurely Choose Failure Policies

Identifying:

Payment provider timeout

does not mean we should immediately decide:

retry exactly three times

First identify the failure mode.

Later determine whether retry is:

safe

unsafe

idempotent

ambiguous

Known risk and implementation policy are different things.


Explicit Technical-Design Questions

A useful working list is:

1. What are our core Domain concepts?

2. Which responsibilities belong to each concept?

3. What are valid Order states and transitions?

4. What data must be persisted?

5. Which data must be historical snapshots?

6. Which operations require transactions?

7. How do we protect Inventory under concurrency?

8. How do UseCases access persistence?

9. How do we isolate Payment provider details?

10. How does authenticated identity enter UseCases?

11. How are business failures represented internally?

12. Which API resources expose the UseCases?

13. Which logical boundaries should remain independent?

14. Which decisions deserve ADRs?

15. Which risks belong in the RFC?

The rest of Module 2 will answer these progressively.


Technical Design Exists at Different Levels

System Level

One deployable backend application

PostgreSQL

Existing identity capability

External Payment provider

Application Level

Product

Inventory

Order

Payment

Security / Identity Context

Workflow Level

How Create Order works

How Cancel Order works

How Pay Order works

Implementation Level

Specific Java classes

Spring annotations

JPA mappings

method signatures

Good design moves from high level to low level intentionally.


Do Not Jump to Method Signatures Too Early

An early signature like:

public OrderResponse createOrder(
        String customerId,
        List<OrderItemRequest> items
) {
    ...
}

looks concrete.

But it may hide unresolved questions:

What is CustomerId?

What does Order own?

What errors can occur?

Where does the transaction live?

How is Inventory concurrency handled?

Does HTTP representation belong
inside the UseCase?

Method signatures come after responsibilities are understood.


Design Should Be Reviewable Before Code

If two engineers disagree about Inventory concurrency strategy, it is cheaper to compare:

Design option A

vs

Design option B

before hundreds of lines of code exist.

Early design review reduces expensive rewrites.


Design Is Communication

Technical design helps:

backend engineers

reviewers

future maintainers

technical stakeholders

understand:

what we propose

why we propose it

which risks exist

which alternatives were rejected

A good design makes important decisions explainable.


Design Effort Should Match Risk

A tiny endpoint may need only:

ticket + implementation note

Our Order Management Backend has:

multiple Domain concepts

transactions

Inventory concurrency

authorization

external Payment integration

That justifies a larger design artifact.

This is why we will write an RFC.


RFC Comes After Initial Reasoning

We should not begin by filling an RFC template mechanically.

First understand:

Domain

responsibilities

architecture

trade-offs

risks

Then the RFC captures the proposal.

That is why Module 2 proceeds through:

From Requirements to Technical Design

Identifying Domain Entities

Defining System Responsibilities

Designing the Initial Architecture

Writing Our First RFC

Architecture Decision Records

Reviewing and Revising a Technical Design

Design Alternatives Matter

Consider Product and Inventory.

Option A

Product contains available quantity.

Option B

Inventory is a separate concept
identified by ProductId.

Both could work.

We evaluate them using:

responsibility

business meaning

change patterns

current requirements

complexity

not:

Which option looks more enterprise?

Our eventual direction keeps Inventory separate because stock lifecycle and Product catalog lifecycle are different concerns.


Alternatives Should Be Real

A weak RFC might list:

PostgreSQL

MongoDB

Cassandra

DynamoDB

Redis

as alternatives even though PostgreSQL is already an engineering constraint.

That is fake decision-making.

Only discuss realistic alternatives for actual open decisions.


Hard Constraints Should Not Be Re-Litigated

We already know:

Java

Spring Boot

Gradle

PostgreSQL

REST / JSON

These are established context.

We should not waste design time asking:

Should we rewrite this in Go?

or:

Should we use MongoDB?

unless the requirements reveal a genuine incompatibility.

They currently do not.


State Non-Goals Explicitly

Our v1 non-goals include:

Microservices

Kafka

Redis

Multiple warehouses

Shipping

Discounts

Coupons

Refunds

Event Sourcing

CQRS

Multiple Payment providers

Identity-platform implementation

These are not bad technologies or features.

They are simply outside the current problem.


Non-Goals Prevent Review Drift

Without non-goals, a reviewer might ask:

How will this design support 50 warehouses?

The current answer is:

It does not need to.

Multiple warehouses are outside v1 scope.

That keeps design review focused.


Technical Design Is About Trade-Offs

Consider separate Inventory.

Benefits:

clear responsibility

independent Inventory changes

clean Product catalog model

ordering logic easier to reason about

Cost:

additional model

additional persistence relationship

cross-capability query for Product browsing

A good design explains why the benefit is worth the cost.


Simplicity Is a Design Property

We want:

minimum necessary complexity

That does not mean:

minimum number of files

and does not mean:

maximum number of abstractions

Simplicity means another engineer can build an accurate mental model of the system without unnecessary indirection.


Optimize for Changeability, Not Imaginary Futures

The system will evolve later.

But we should not design for unknown future features.

Instead, choose boundaries that are useful now and naturally make future change manageable.

For example:

Payment provider details are isolated

because external integration already justifies that.

We do not need five providers to justify the boundary.

Likewise:

Product current price

and:

OrderItem historical price

are separate because the current requirement already demands it.


YAGNI Does Not Mean “Don't Design”

YAGNI means:

You Aren't Gonna Need It

It warns against speculative functionality.

It does not mean:

Ignore transactions.

Ignore failures.

Ignore concurrency.

Ignore responsibility boundaries.

Those concerns are necessary for current correctness.


Overengineering vs Necessary Engineering

Overengineering:

Add Kafka because commerce systems
often use Kafka.

Necessary engineering:

Ensure Order creation cannot leave
Inventory consumed without an Order.

Overengineering:

Build a generic Payment-provider
plugin framework.

Necessary engineering:

Keep provider HTTP details outside
Order business logic.

This distinction should guide the project.


Design Decisions Should Trace Back to Requirements

Decision:

OrderItem stores unitPriceCents.

Reason:

Historical Order pricing must remain unchanged.

Decision:

Order creation uses one local transaction.

Reason:

Order creation is all-or-nothing.

Decision:

Order access is scoped by CustomerId.

Reason:

Customers may access only their own Orders.

Decision:

Inventory is separate from Product.

Reason:

Product catalog state and Inventory state
have different responsibilities and lifecycles.

Traceability makes design easier to review.


Initial Technical Risk Register

Risk 1 — Inventory Concurrency

Concurrent Order requests may attempt
to consume the same remaining Inventory.

The design must protect Inventory correctness.

Risk 2 — Payment Partial Failure

External Payment may succeed
while local Order state update fails.

A local database transaction cannot solve this alone.

Risk 3 — Authorization Mistakes

Incorrect ownership enforcement could expose
one Customer's Orders to another Customer.

Security design and tests must protect against this.

Risk 4 — Historical Data Drift

Changing current Product information
must not rewrite historical Order meaning.

Purchase-time data must remain stable.

Risk 5 — Scope Expansion

Commerce requirements can easily expand into
shipping, refunds, discounts, warehouses,
and unrelated capabilities.

The design must stay inside v1 scope.


Risk Is Not the Same as an Existing Bug

For example:

Payment partial failure

is a known risk because we cross an external boundary.

It does not mean the application already has a Payment bug.

Risk tells us where design attention is needed.


Architecture Decisions vs Implementation Details

Architecture decision:

Use one deployable modular application.

Implementation detail:

Name a package io.tarkev.orders.

Architecture decision:

Payment integration is isolated
behind PaymentService.

Implementation detail:

Name the provider client class
PaymentProviderClient.

ADRs should capture meaningful decisions, not every naming choice.


Design Should Leave Room for Implementation

Too vague:

The system will be clean,
scalable, and enterprise-ready.

Too detailed too early:

Every private method
inside CreateOrderUseCase.

Good technical design describes:

responsibilities

flows

boundaries

state transitions

data ownership

transaction requirements

failure modes

risks

major decisions

Implementation fills in the code-level details.


Our Initial Design Direction

Without finalizing later lessons, we can already state the direction.

We will build:

one Spring Boot backend application

with logical areas around:

Product

Inventory

Order

Payment

Security / Customer Identity Context

Application-owned persistent state uses:

PostgreSQL

Internal workflows are coordinated through:

UseCases

Persistence access uses:

Repositories

External Payment communication remains behind:

PaymentService

Authenticated identity enters from the existing identity capability and becomes Customer context.

Important local multi-state operations use transactional consistency.

This is direction, not final implementation structure.


What We Still Need to Determine

Before writing the RFC, we still need to answer:

Which Domain concepts are Entities?

Which concepts are value-like?

What exactly does Order own?

What exactly does Inventory own?

How should Product and Inventory interact?

Which responsibilities belong to each capability?

How should UseCases and Repositories interact?

Where should persistence interfaces live?

What should the package/module architecture look like?

Which decisions deserve ADRs?

Those are the next lessons.


Technical Design Review Questions

Requirements

Does the design satisfy
the relevant Acceptance Criteria?

Scope

Does it avoid unrelated functionality?

Responsibilities

Is it clear who owns each behaviour?

Data

Is data ownership clear?

Integrity

Are important invariants protected?

Transactions

Are local multi-state operations
kept consistent?

Failures

Are important failure modes recognized?

Security

Are identity, ownership,
and authorization boundaries clear?

Dependencies

Are external systems isolated appropriately?

Maintainability

Can another engineer understand
how the system works?

These questions matter more than how sophisticated the diagram looks.


A Technical Design Is a Hypothesis

A useful mindset is:

Technical design is our best current hypothesis for how to satisfy known requirements.

It is not sacred.

During implementation, we may discover:

an assumption was wrong

a framework behaves differently

a query performs poorly

a boundary is awkward

a requirement was misunderstood

Then the design can change.

Good engineering makes decisions explicitly and revises them when evidence changes.


Design Should Not Change Randomly

Healthy revision:

Original design:
Inventory uses approach A.

New finding:
Approach A cannot preserve
Inventory correctness under concurrency.

Decision:
Use approach B.

Reason:
Protect the Inventory invariant.

That is evidence-driven evolution.

This is not:

I saw a new architecture pattern
and rewrote everything.

Design and Backlog Influence Each Other

The backlog already contains work such as:

Model Order and OrderItem Domain

Implement Order Creation Workflow

Technical design may reveal:

a ticket should be split

or:

a technical dependency needs
its own work item

For example, concurrency-safe Inventory consumption may deserve explicit implementation work.

The backlog becomes more precise as the design becomes more precise.


Avoid Architecture Theatre

Our project will use:

RFCs

ADRs

system diagrams

engineering tickets

only when they improve engineering thinking.

We are not creating artifacts merely to imitate a large company.

The goal remains:

build and operate
a correct backend system

not:

produce the maximum
amount of documentation

Engineering Principle

The core principle from this lesson is:

Requirements define the behaviour we owe the user; technical design defines how responsibilities, data, boundaries, transactions, and failure handling work together to provide that behaviour.

Another important principle:

Start technical design from business behaviour and system constraints—not from framework annotations, database tables, or fashionable architecture patterns.

And:

Use the minimum complexity necessary to protect the current system's known behaviour and risks.


Where We Are Now

Our journey is:

Requirements
    ↓
Acceptance Criteria
    ↓
Engineering Context
    ↓
Backlog
    ↓
Technical Design

Technical design has now begun.

But we have intentionally stayed above code-level implementation detail.

Our next step is:

Identify the Domain concepts
that make up the system.

Summary

In this lesson, we learned that:

  • Requirements describe what the system must do; technical design describes how the system will provide that behaviour.
  • Technical design is more than an architecture diagram.
  • Design should derive from requirements, constraints, risks, and trade-offs.
  • Framework-first architecture should be avoided.
  • UseCases expose application responsibilities.
  • Internal workflows use UseCase; Service is reserved for external integrations.
  • Domain modelling should come before database modelling.
  • Customer identity is authenticated application context, not a local Customer-management subsystem.
  • Important invariants are core design inputs.
  • Different rules need enforcement at different boundaries.
  • Order creation and cancellation are local transactional workflows.
  • External Payment is fundamentally different because local database transactions cannot roll back remote side effects.
  • Logical boundaries are useful even inside one deployable application.
  • Product, Inventory, Order, Payment, and Security/Identity Context have different responsibilities.
  • Historical OrderItem.unitPriceCents and current Product.priceCents represent different facts.
  • Money uses long cents in Java and BIGINT in PostgreSQL.
  • The Order lifecycle is limited to UNPAID, PAID, and CANCELLED.
  • APIs should expose business operations rather than mirror database tables.
  • Clients should not orchestrate backend transactions.
  • Persistence contracts should emerge from real UseCases.
  • Database constraints can reinforce persisted invariants.
  • Inventory concurrency must be considered before implementation.
  • Expected business failures should remain distinct from unexpected system failures.
  • Payment provider details belong behind an external integration boundary.
  • Identity-provider details should not leak into Domain logic.
  • Authorization includes application-level ownership, not only HTTP annotations.
  • Technical design should recognize read patterns, write patterns, and failure points.
  • Non-goals prevent design from expanding into unrelated scope.
  • Alternatives should be discussed only when a real decision exists.
  • YAGNI discourages speculative functionality; it does not excuse ignoring known correctness problems.
  • Design decisions should be traceable to requirements.
  • Technical design can evolve when evidence changes.
  • Technical design and backlog refinement inform each other.
  • Engineering artifacts should improve decisions rather than create architecture theatre.

Next lesson:

Identifying Domain Entities

There, we will analyze Product, Inventory, Order, OrderItem, CustomerId, and the Payment boundary to determine which concepts have identity and lifecycle, which are value-like, what each concept owns, and which relationships and invariants belong in the Domain model.