Designing the System

From Requirements to Technical Design

ReadingPreview

আপনি একটি free preview lesson দেখছেন।

Module 1-এ আমরা একটি গুরুত্বপূর্ণ কাজ করেছি।

আমরা coding শুরু করার আগে বুঝেছি:

  • business কী চায়
  • user কী করতে পারবে
  • কোন behaviour required
  • কোন behaviour out of scope
  • কোন engineering constraints already exist
  • একটি change কখন Done বলা যাবে
  • initial engineering backlog দেখতে কেমন হবে

এখন আমাদের কাছে problem-এর যথেষ্ট context আছে।

Next question:

এই requirements satisfy করার জন্য system-টি কীভাবে design করা উচিত?

এই জায়গা থেকেই শুরু হয় Technical Design

Technical design-এর purpose হলো class diagram বানিয়ে impressive architecture দেখানো নয়।

Purpose হলো implementation-এর আগে গুরুত্বপূর্ণ technical decisions sufficiently পরিষ্কার করা, যাতে team একটি coherent system build করতে পারে।

এই lesson-এ আমরা requirements থেকে technical design-এ যাওয়ার reasoning process শিখব।

আমরা এখনও final database schema, package structure বা Java classes লিখব না।

প্রথমে বুঝব design আসলে কোন questions-এর answer দিতে হবে।


Requirements Tell Us What; Design Decides How

আমাদের একটি requirement:

Customers can create an order containing
one or more available products.

আরও কিছু behaviour:

Inventory must be sufficient.

The backend determines prices.

Order creation is all-or-nothing.

Purchase-time prices must be preserved.

এই requirements আমাদের বলে system কী behaviour provide করবে।

কিন্তু এগুলো বলে না:

Which components should exist?

Where should order creation logic live?

How should products and inventory interact?

What should be stored in PostgreSQL?

What belongs inside an Order?

Where should transaction boundaries exist?

How should external payment logic connect to orders?

এই questions technical design-এর অংশ।

Conceptually:

Requirement
    ↓
What must happen?
Technical Design
    ↓
How should our system make it happen?

Technical Design Is More Than Architecture Diagram

Technical design শুনলে অনেকেই প্রথমে boxes and arrows ভাবেন।

For example:

Controller
   ↓
Service
   ↓
Repository
   ↓
Database

এটি architecture-এর একটি small অংশ হতে পারে।

কিন্তু meaningful technical design আরও অনেক question answer করে।

For example:

What are the core domain concepts?

Who owns which responsibility?

Which rules must remain true?

Where do transactions begin and end?

Which data must be persisted?

Which system boundaries exist?

How are failures handled?

Which decisions are difficult to reverse?

What alternatives did we consider?

What are the known risks?

Boxes আঁকা easy।

Correct responsibility এবং behaviour define করা harder।


Start With the Problem, Not the Framework

Bad starting point:

We are using Spring Boot.

Therefore we need:

Controller
Service
Repository
Entity
DTO
Mapper

এই approach framework structure দিয়ে design শুরু করে।

Better:

What business behaviour are we implementing?

Then:

What responsibilities are required?

Then:

How should those responsibilities interact?

Finally:

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

Framework design follow করবে।

Design framework follow করবে না।


Our Design Inputs

Technical design শূন্য থেকে তৈরি হবে না।

আমাদের already several inputs আছে।

Product Requirements

For example:

Customers browse products.

Customers place orders.

Customers view order history.

Customers cancel eligible orders.

Customers pay through a payment provider.

Administrators manage products and inventory.

Acceptance Criteria

For order creation:

Order must contain at least one item.

Products must exist.

Products must be available for ordering.

Quantities must be positive.

Inventory must be sufficient.

Backend determines prices.

Historical purchase-time prices are preserved.

Any invalid item causes the whole operation to fail.

Engineering Context

Java

Spring Boot

Gradle

REST / JSON

PostgreSQL

Flyway

Existing authentication capability

External payment provider

Containerized deployment

Non-Functional Concerns

security

data integrity

reliability

maintainability

observability

Design must satisfy all of these together।


Good Design Is Constraint Satisfaction

একটি useful way to think about system design:

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

For example:

Requirement:

Order creation must be all-or-nothing.

Engineering context:

Order and inventory are stored
in the same PostgreSQL-backed application.

This gives us a strong clue:

A database transaction may be an appropriate mechanism.

Notice language:

may be appropriate

We are deriving a design direction from requirements and context।

We are not saying:

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

Design Decisions Have Reasons

A design decision without reasoning is difficult to evaluate।

Example:

We will store order-item unit price.

Why?

Because requirement says:

Historical order prices must not change
when catalog prices change.

Now decision is traceable:

Requirement
    ↓
Historical pricing must remain stable
    ↓
Order item needs purchase-time pricing information

That reasoning later belongs in design documentation।


Identify the Core Use Cases First

Before components, classes, and tables, identify the workflows our backend must support।

Our major v1 use cases are:

Browse Products

Manage Products

Manage Inventory

Create Order

View Order History

Cancel Order

Pay for Order

These use cases are useful because they expose system responsibilities।


Example: Create Order Use Case

Let's look at Create Order.

From acceptance criteria, conceptually the workflow must do something like:

Receive requested items
        ↓
Identify customer
        ↓
Validate request
        ↓
Load referenced products
        ↓
Ensure products are orderable
        ↓
Validate quantities
        ↓
Check available inventory
        ↓
Determine current product prices
        ↓
Create order with purchase-time prices
        ↓
Adjust inventory
        ↓
Persist order

This is not yet implementation।

It is a behavioural flow

We can now start asking:

Which responsibility belongs where?

Identify Responsibilities Before Classes

From the order flow we can identify responsibilities such as:

Represent an order

Represent an order item

Determine whether an order can be cancelled

Calculate order total

Retrieve product information

Check inventory

Change inventory

Persist orders

Coordinate the order creation workflow

Later these responsibilities may become:

classes
methods
services
repositories
domain objects

But we should not jump there immediately।

First understand responsibility।


Responsibility Is More Stable Than Class Names

Suppose we decide:

OrderService

Then later perhaps rename it:

OrderApplicationService

or split responsibilities।

Class name can change।

But underlying responsibilities remain:

coordinate order creation

enforce ordering rules

persist resulting state

Design discussion should focus primarily on these responsibilities।


Start With Domain Concepts

Our requirements repeatedly mention certain business concepts।

For example:

Customer

Product

Inventory

Order

Order Item

Payment

These are candidates for domain concepts।

But not every noun automatically becomes an entity or class।

That decision requires thinking।

For example:

Price

is also a domain concept।

Should Price be:

BigDecimal field?

or:

Money value object?

We haven't decided yet।

Similarly:

Order Status

might be an enum or richer state model।

The point is:

Requirement language gives us clues, not final code structure.


Domain Model vs Database Model

A common mistake is to start technical design with tables।

For example:

customers
products
orders
order_items
inventory
payments

These may eventually exist।

But database schema is only one representation of the domain।

Better sequence:

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

Why?

Because if we start with tables, we often model data storage before understanding behaviour।


Example: Order Is More Than a Table

If we think only in database terms:

orders

id
customer_id
status
total

Then code may become:

order.setStatus(CANCELLED);

from anywhere।

But requirement says cancellation has rules:

Only unpaid orders can be cancelled.

Cancelled orders cannot be cancelled again.

Cancellation restores inventory.

This means Order is not just data।

It has behaviour and invariants।

Later domain design should reflect that।


Identify Invariants

Technical design should explicitly identify rules that must remain true।

For our current system, important invariants include:

An order must contain at least one item.
Order item quantity must be positive.
A successful order must not consume
more inventory than is available.
Order total must be consistent
with the prices and quantities recorded for the order.
A cancelled order cannot be paid.
A paid order cannot be cancelled in v1.
A customer cannot operate on another customer's order.

These invariants are design inputs।


Why Invariants Matter

Suppose invariant:

Inventory cannot become negative.

If we enforce it only in controller validation:

Controller checks inventory.

Then another code path may bypass that controller।

Or concurrent requests may invalidate the check before update।

So design question becomes:

Where can this invariant be enforced reliably?

This can influence:

domain rules

database constraints

transaction boundaries

concurrency strategy

Design decisions should protect invariants at appropriate layers।


Not Every Rule Belongs in the Same Layer

Consider:

quantity must be positive

We might enforce it in:

request validation

and perhaps:

domain construction

because invalid quantity should ideally not become valid domain state।

Meanwhile:

customer can only access own order

depends on authenticated user context and belongs largely to application/security boundaries।

Meanwhile:

inventory cannot become negative under concurrent ordering

may require persistence/transaction support।

This is why design is not simply:

all validation goes in controller

or:

all rules go in service

Different rules have different enforcement needs।


Identify Transactional Workflows

Some operations modify several pieces of state together।

Order creation:

Create order
+
Persist order items
+
Adjust inventory

Cancellation:

Mark order cancelled
+
Restore inventory

These operations have atomicity requirements।

From our acceptance criteria:

Order creation is all-or-nothing.

Therefore technical design must define a transaction boundary।

Conceptually:

BEGIN
    validate state required for mutation
    update inventory
    save order
    save items
COMMIT

If failure:

ROLLBACK

Exact ordering and implementation later।

But requirement already tells us these changes should not partially commit।


Payment Is Different

Payment is more complex because one part of the workflow is external।

Conceptually:

Our Database
     +
External Payment Provider

A PostgreSQL transaction cannot automatically roll back an external provider call।

This means:

Database transaction

alone cannot solve the entire payment consistency problem।

This is an important design observation।

We don't need to solve the full integration behaviour in this lesson।

But technical design should recognize:

External side effects require different reasoning from local database updates.


System Boundaries Matter

Our system context already established:

Existing Identity Capability
        ↓
Order Management Backend
        ↓
Payment Provider

and:

Order Management Backend
        ↓
PostgreSQL

Technical design must preserve these boundaries।

For example:

Order Management Backend should not:

store user passwords

implement card-network processing

own external identity lifecycle

Boundary prevents scope expansion।


Internal Boundaries Matter Too

Even inside one deployable application, responsibilities should have separation।

Conceptually:

Product
Inventory
Order
Payment
Customer Context

These are not microservices।

They are logical parts of the same application।

We want:

one deployable application

while still avoiding:

one giant undifferentiated codebase

Modular Thinking Without Microservices

A useful initial model:

Order Management Backend

├── Product
├── Inventory
├── Customer
├── Order
└── Payment

Each area should have clear responsibilities।

For example:

Product

product identity
product price
orderable/inactive state

Inventory

available quantity
inventory adjustment
inventory availability

Order

order lifecycle
order items
historical price
order total
cancellation rules

Payment

order-related payment workflow
external provider integration boundary

Customer Context

customer identity relevant to ordering

These boundaries are logical design aids।


Avoid Premature Layer Explosion

A common Java backend architecture can 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 starting question:

What is the simplest structure that keeps responsibilities understandable and protects our requirements?

We will not create layers just because enterprise Java projects sometimes have them।


But Avoid One Giant Service Too

Opposite extreme:

public class CommerceService {
    createProduct();
    updateInventory();
    createOrder();
    cancelOrder();
    payOrder();
    getOrderHistory();
}

This combines unrelated responsibilities।

As application grows, this becomes hard to reason about and test।

So design requires balance:

not too fragmented

and:

not too centralized

Use Cases Help Define Application Boundaries

One possible design style is to organize application-level behaviour around use cases।

For example:

CreateProduct

BrowseProducts

AdjustInventory

CreateOrder

CancelOrder

PayOrder

GetOrderHistory

We are not yet saying each one must be a separate Java class।

But this vocabulary helps keep workflows distinct।


Domain Behaviour vs Workflow Coordination

This distinction will matter throughout the course।

Consider cancellation.

Some behaviour belongs naturally to Order:

Can this order be cancelled?

What state transition is valid?

But cancellation workflow also needs:

load order

verify ownership

restore inventory

save changes

Order itself should probably not connect to PostgreSQL or call repositories।

So we can think:

Domain
    ↓
knows business rules
Application workflow
    ↓
coordinates domain + persistence + dependencies

This separation is a central backend design idea।


Example: Order Cancellation

Conceptually:

CancelOrder Use Case
        ↓
load order
        ↓
verify customer ownership
        ↓
ask Order to cancel
        ↓
restore inventory
        ↓
persist changes

The Order domain concept may know:

PAID order cannot become CANCELLED.

But application workflow knows:

which authenticated customer is making the request

and:

which repositories to use

and:

that inventory restoration must happen
within the operation

This division keeps business rules and orchestration understandable।


Technical Design Should Define Data Ownership

For each important piece of state, ask:

Who owns this data?

For example:

Product price
→ Product capability
Available inventory quantity
→ Inventory capability
Purchase-time unit price
→ Order

Notice:

Current product price and historical order price are not the same thing।

This is a crucial design distinction।


Current Price vs Historical Price

Suppose:

Product P1

Current price: €20

Customer creates order.

Order item records:

Product P1
Quantity: 2
Unit price at purchase: €20

Later:

Product P1

Current price: €25

Product owns current catalog price।

Order owns historical transaction price।

So:

Product.price

and:

OrderItem.unitPrice

represent different facts।

Duplicating data is not automatically wrong if the data represents different historical meaning।


Avoid Database Normalization Dogma

Someone might say:

"Why store price twice? We can always join Product."

That would violate our historical pricing requirement।

The issue is not simply database normalization।

The issue is semantics।

Product.price
=
price now
OrderItem.unitPrice
=
price when order was created

Different facts deserve separate persistence।


Identify Relationships

Our current domain relationships conceptually include:

Customer
   1
   │
   │ owns
   ▼
   *
Order
Order
   1
   │
   │ contains
   ▼
   *
Order Item
Order Item
   *
   │
   │ references
   ▼
   1
Product
Product
   1
   │
   │ has relevant inventory
   ▼
   1
Inventory

We haven't defined database foreign keys yet।

This is domain relationship thinking।


Relationships Have Lifecycle Questions

Consider:

Product → Inventory

Questions:

Does every product always have inventory?

Can inventory exist without a product?

What happens when product is deactivated?

Can inventory still be adjusted?

Similarly:

Product → Order Item

If product is deactivated:

historical order item must remain valid

That immediately argues against destructive behaviour that would invalidate order history।


Deactivation vs Deletion

Our requirement says product can be deactivated।

Physical deletion is out of scope।

Why is this useful?

Because historical order records may reference a product।

If we physically remove product data carelessly:

Order history
    ↓
broken reference / missing context

Deactivation preserves:

future ordering disabled

while:

historical data remains valid

This is a design choice grounded in requirements।


Design the State Transitions

Order behaviour depends heavily on state।

For v1 we only need states sufficient for:

unpaid
paid
cancelled

Conceptually:

UNPAID
  │
  ├──── payment succeeds ───▶ PAID
  │
  └──── cancel ─────────────▶ CANCELLED

And:

PAID
  ──X──▶ CANCELLED

for v1।

Also:

CANCELLED
  ──X──▶ PAID

This state transition view is much more useful than randomly adding enum values।


State Machine Thinking

Even if we do not use a formal state-machine library, state-machine thinking is useful।

Ask:

What states exist?

Which transitions are valid?

What event causes each transition?

Which transitions are forbidden?

For our current scope:

UNPAID → PAID

valid after confirmed successful payment।

UNPAID → CANCELLED

valid after eligible cancellation।

PAID → CANCELLED

not supported in v1।

CANCELLED → PAID

invalid।

This makes domain rules explicit।


Don't Model Future States "Just in Case"

Avoid:

SHIPPED
DELIVERED
RETURNED
REFUNDED

because they sound realistic।

Current system does not support fulfilment or refunds।

Every extra state creates:

more transitions
more validation
more tests
more ambiguity

Design only what the current scope requires।


Design API Boundaries From Use Cases

We know APIs will be REST/JSON।

Technical design should map business capabilities to resources carefully।

Potential resource concepts:

/products
/orders
/inventory

But endpoint details should follow behaviour।

For example order creation naturally suggests:

POST /orders

Order history:

GET /orders

Order details:

GET /orders/{orderId}

Cancellation might be action-oriented:

POST /orders/{orderId}/cancel

We will design the API properly in the REST module।

At this stage, we only identify likely resource boundaries।


Don't Let Database Shape Leak Into the API

Suppose table:

order_items

That does not mean we need public endpoint:

POST /order-items

Order items exist as part of an order।

Business behaviour says customer creates an order containing items।

So API should reflect business operation, not table CRUD।


CRUD Is Not the Domain

Product administration may map naturally to CRUD-like operations।

But ordering does not。

Bad design:

POST /orders

POST /order-items

PATCH /inventory

PATCH /orders/status

leaves business workflow responsibility to client।

Better:

Create Order

as one business operation।

Backend coordinates required changes।

Client should not manually orchestrate domain invariants।


Avoid Client-Orchestrated Transactions

Bad client workflow:

1. GET product price

2. GET inventory

3. PATCH inventory -2

4. POST order

5. POST order item

Many things can go wrong between calls।

Client may fail after step 3।

Two clients may race।

Client may send wrong price।

Instead:

POST /orders

Backend owns the business transaction।

This is a fundamental backend responsibility।


Design Persistence Around Behaviour

Persistence must support our use cases efficiently enough।

For example:

Find product by ID

Find inventory by product

Save order and items

Find customer's orders

Find order by ID

Update order status

We do not need repository methods like:

findEverythingByAnything(...)

before real use cases exist।

Design repository access around actual application needs।


Persistence Abstraction

Application/domain code should not ideally contain SQL everywhere।

Spring Data JPA may provide repository abstractions later।

But abstraction should still represent meaningful persistence operations।

For example:

findById
save
findByCustomer

Exact interfaces come later।

The design goal:

business workflow

should not be dominated by low-level database mechanics।


Database Constraints Can Protect Invariants

Some invariants can be reinforced at database level।

Example:

inventory quantity >= 0

Potential database constraint:

CHECK (quantity >= 0)

This does not replace application validation।

It provides an additional integrity boundary।

Similarly:

required values

may use:

NOT NULL

Technical design should consider which rules deserve persistence-level protection।


But Database Constraints Cannot Express Everything Elegantly

Example:

A paid order cannot be cancelled.

This is a domain transition rule।

Trying to encode every lifecycle rule entirely in database constraints could become awkward।

Application/domain logic is a more natural place for many behavioural rules।

Again:

Different invariants belong at different boundaries.


Data Integrity Through Multiple Layers

For quantity:

API validation
        ↓
reject obvious invalid input
Domain validation
        ↓
prevent invalid business object state
Database constraint
        ↓
protect persisted integrity

This is not necessarily unnecessary duplication।

Each layer protects a different boundary।

But we should not blindly repeat every rule everywhere।

Use judgement based on risk।


Concurrency Must Be Recognized in Design

Suppose inventory:

1 unit available

Two requests arrive at nearly the same time:

Request A wants 1
Request B wants 1

Naive flow:

A reads inventory = 1

B reads inventory = 1

A validates success

B validates success

A subtracts 1

B subtracts 1

Depending on implementation, we may:

oversell

or lose an update।

So requirement:

Inventory must not become negative
or oversell available quantity.

has a concurrency consequence।

Technical design must acknowledge it।


Don't Solve Concurrency With "Check Then Update" Alone

This pseudo-code is insufficient by itself:

if (inventory.getQuantity() >= requestedQuantity) {
    inventory.setQuantity(
        inventory.getQuantity() - requestedQuantity
    );
}

Single-threaded reasoning looks correct।

Concurrent database requests can make it wrong।

Later transaction/persistence lessons will implement a safe strategy।

For now design should record:

Inventory mutation requires concurrency-safe persistence behaviour.


Design for Atomic Order Creation

Our accepted behaviour:

If any item is invalid,
the entire order creation fails.

Suppose request contains:

Product A × 2
Product B × 3

If A succeeds but B has insufficient inventory:

No order should be created.

No inventory should remain changed.

Therefore order creation is one transactional unit from business perspective।

Technical design should reflect that explicitly।


Error Handling Is Part of Design

When business rule fails:

unknown product

or:

insufficient inventory

or:

order cannot be cancelled

we should not let random exception behaviour leak to API clients।

We need a consistent error model।

Exact API error format comes later।

But architecture should distinguish:

expected business failure

from:

unexpected system failure

Business Failure vs System Failure

Business failure:

Requested quantity exceeds inventory.

System is functioning correctly।

It is rejecting an invalid business operation।

System failure:

Database connection unavailable.

or:

Unexpected NullPointerException.

These are operational failures।

The distinction affects:

HTTP mapping
logging
metrics
retry behaviour

Technical design should preserve it।


External Integration Boundary

Payment provider should be isolated behind a clear application-facing boundary।

Why?

Without a boundary:

OrderService
    ↓
HTTP request building
JSON parsing
provider status codes
provider credentials

all mixed together।

Then order business logic depends directly on one provider's protocol।

Better conceptual design:

Order Payment Workflow
        ↓
Payment Gateway
        ↓
Provider HTTP Client

Application workflow knows:

request payment
receive payment outcome

Provider adapter knows:

URL
HTTP
provider request schema
provider response schema

This keeps responsibilities clearer।


Don't Create Abstraction for Imaginary Providers

We are not building:

StripePaymentProvider
AdyenPaymentProvider
PayPalPaymentProvider
BankPaymentProvider

without requirement।

We have one payment provider।

The boundary exists because external integration concerns differ from order logic, not because we are predicting five future providers।

Important distinction।


Identity Boundary

Existing identity capability authenticates users।

Our application needs something like:

authenticatedUserId
roles / permissions

The domain should not depend on:

JWT parsing details

everywhere।

Spring Security/infrastructure layer can translate authentication context into application-relevant identity information।

Then use cases can reason about:

customerId

or:

administrator permission

without caring how authentication token was technically validated।


Authorization Is a Business Boundary Too

Suppose customer requests:

GET /orders/123

Order 123 belongs to another customer।

From security perspective access should be blocked।

From application perspective:

resource ownership matters.

So authorization is not merely a controller annotation concern।

Sensitive use cases should remain ownership-aware even if HTTP security configuration exists।


Technical Design Should Consider Read Patterns

Our system needs to:

browse available products

and:

view customer order history

These are list operations।

Therefore design should avoid assumption:

load every record in the database

We already established bounded retrieval।

Persistence/API design should support pagination।

Exact pagination style later।


Technical Design Should Consider Write Patterns

Important writes:

create product

adjust inventory

create order

cancel order

mark payment successful

Some writes are simple।

Some involve multiple domain changes।

We should identify them because transaction strategy differs।

For example:

Update product name

simple।

Create order and deduct inventory

transactional multi-state workflow।

Pay order through external provider

cross-system workflow।

Not all writes are equivalent।


Think in Failure Points

For each workflow, ask:

Where can this fail?

Order creation:

invalid request

product missing

inactive product

insufficient inventory

database failure

Cancellation:

order missing

wrong customer

already cancelled

already paid

database failure

Payment:

order missing

wrong customer

ineligible state

provider rejects

provider timeout

database update failure

Failure analysis is part of design।


Do Not Design Every Failure Mechanism Yet

Identifying:

provider timeout

does not mean we must immediately choose:

3 retries with exponential backoff

That would be premature।

We first identify the failure mode।

Later integration design decides whether retry is safe।

This separation prevents accidental assumptions।


Design Questions Should Be Explicit

A useful technical-design working list for our system:

1. What are our core domain concepts?

2. Which responsibilities belong to each concept?

3. What are the valid order states and transitions?

4. What data must be persisted?

5. What data must be historical snapshots?

6. Which operations need transactions?

7. How do we protect inventory under concurrency?

8. How does application logic access persistence?

9. How do we isolate payment-provider details?

10. How does authenticated identity enter application workflows?

11. How are business errors represented internally?

12. Which API resources expose the use cases?

13. Which boundaries must remain independent?

14. Which decisions deserve ADRs?

15. Which risks should the RFC call out?

Module 2 will answer these progressively।


Technical Design Has Different Levels

Not every design question has the same scope।

System-Level

One deployable backend application

PostgreSQL

External payment provider

External identity capability

Application-Level

Product area

Inventory area

Order area

Payment area

Workflow-Level

How Create Order works

How Cancel Order works

How Payment works

Implementation-Level

Specific Java classes

Spring annotations

JPA mappings

method signatures

Good design moves from high-level to low-level intentionally।


Don't Jump to Method Signatures Too Early

Bad early design:

public OrderResponse createOrder(
    Long customerId,
    List<OrderItemRequest> items
)

This looks concrete but hides unresolved questions:

What is Order?

How is customer represented?

What errors can occur?

How does transaction work?

How is inventory concurrency handled?

Method signatures should come after core responsibilities are understood।


Design Should Be Reviewable Before Code

One major reason for technical design:

Suppose two engineers disagree about inventory strategy।

It is much cheaper to discuss:

Design option A vs B

before writing hundreds of lines of code।

Once implementation exists, humans naturally become attached to it।

Reviewing design early reduces expensive rewrites।


Design Is Communication

Technical design is not only for the author।

It helps:

other backend engineers

reviewers

future maintainers

product/technical stakeholders

understand the proposed system।

A good design should make important decisions explainable।


Design Must Be Proportional to Risk

A tiny endpoint may need:

ticket + brief implementation note

Our initial Order Management Backend has:

multiple domain concepts

transactions

inventory consistency

security

external payment integration

So a larger design artifact is justified।

This is why our plan includes an RFC।


RFC Comes After Initial Reasoning

We should not open an RFC template and start filling headings mechanically।

First we need to understand:

domain

responsibilities

architecture

trade-offs

Then RFC captures the resulting proposal।

That is why Module 2 order is:

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

Each lesson builds toward the design document।


Design Alternatives Matter

Suppose inventory and product could be modeled:

Option A

Product contains inventory quantity.

Option B

Inventory is a separate domain concept.

Both may work।

We need to evaluate based on:

responsibility

change patterns

business meaning

future current scope

We should not choose based only on:

Which one looks more enterprise?

Later design lessons will reason about these choices।


Alternatives Do Not Mean Listing Every Possibility

Bad RFC:

Alternative 1: PostgreSQL
Alternative 2: MongoDB
Alternative 3: Cassandra
Alternative 4: DynamoDB
Alternative 5: Redis

even though engineering context already mandates PostgreSQL।

This is fake decision-making।

Only discuss realistic alternatives for actual open decisions।


Hard Constraints Should Not Be Re-Litigated Without Reason

We already know:

Java
Spring Boot
PostgreSQL
Gradle
REST/JSON

These are engineering context।

Our design should not waste time asking:

Should we rewrite it in Go?

or:

Should we choose MongoDB?

unless requirements reveal a genuine incompatibility।

They currently do not।


Design Should State Non-Goals

Technical design often becomes clearer when we explicitly say what it does not solve।

Our v1 non-goals include:

Microservices

Kafka

Redis

Multiple warehouses

Shipping

Discounts

Coupons

Refunds

Event sourcing

CQRS

Multiple payment providers

Identity-platform implementation

These aren't "bad technologies/features."

They are simply not part of the current problem।


Non-Goals Prevent Review Drift

Without non-goals, reviewer might ask:

"How will this design handle 50 warehouses?"

Current answer:

It does not need to.
Multiple warehouses are outside v1 scope.

This keeps design evaluation aligned with actual requirements।


Technical Design Is About Trade-Offs

There is rarely a single perfect design।

For example:

Separate inventory concept:

Benefits:

clear responsibility
independent inventory changes
ordering logic easier to reason about

Costs:

additional model and persistence relationship

A technical design should explain why benefits justify costs।


Simplicity Is a Design Property

A solution can satisfy requirements while remaining easier to understand।

We want:

minimum necessary complexity

not:

minimum number of files at all costs

and not:

maximum number of abstractions

Simplicity means engineers can form an accurate mental model of the system।


Optimize for Changeability, Not Hypothetical Futures

Our system will change later।

We already know Module 14 introduces a change request।

But we won't design directly for an unknown future feature।

Instead we want sensible boundaries so future changes are manageable।

Example:

Keeping payment provider details isolated improves changeability now, even without assuming multiple future providers।

Keeping product current price separate from order historical price protects current requirement and future changes naturally।


YAGNI Does Not Mean "Don't Design"

YAGNI:

You Aren't Gonna Need It

usually warns against speculative functionality।

It does not mean:

Don't think about transactions.

Don't think about failures.

Don't define responsibilities.

Those are required by current system correctness।

Designing for actual known risks is not overengineering।


Overengineering vs Necessary Engineering

Overengineering:

Add Kafka because commerce systems often use Kafka.

Necessary engineering:

Ensure order creation cannot leave inventory
updated without an order.

Overengineering:

Create generic plugin framework for payment providers.

Necessary engineering:

Separate provider HTTP details from order business logic.

This distinction will guide our design।


Design Decisions Should Be Traceable to Requirements

Example:

Decision:
Order item stores purchase-time unit price.

Trace:

Requirement:
Historical order pricing must not change.

Decision:
Order creation executes as one local transaction.

Trace:

Requirement:
Order creation is all-or-nothing.

Decision:
Order access uses customer ownership checks.

Trace:

Requirement:
Customers can access only their own orders.

This traceability makes design easier to justify।


Technical Risk Register

At this point we can create an initial risk list।

Risk 1 — Inventory Concurrency

Concurrent order requests may attempt to consume
the same remaining inventory.

Design must protect inventory consistency।


Risk 2 — Payment Partial Failure

External payment may succeed while local state update fails.

This cannot be treated like a simple local database transaction।


Risk 3 — Authorization Mistakes

Incorrect ownership checks could expose
one customer's orders to another customer.

Security design and tests must protect this।


Risk 4 — Historical Data Drift

Current product changes must not rewrite
historical order meaning.

Order snapshots must preserve relevant historical data।


Risk 5 — Scope Expansion

Commerce domain can easily grow into
shipping, discounts, refunds, warehouses, etc.

Design must stay within current v1 scope।


Risk Is Not the Same as Problem

For example:

Payment partial failure

is a known risk because we cross an external boundary।

It does not mean our current system already has a production bug।

Risk informs design attention।


Architecture Decisions vs Implementation Details

Potential architecture decision:

Use one deployable modular application.

Potential implementation detail:

Name Java package io.example.orders.

Architecture decision:

Payment provider integration is isolated
behind an application-facing boundary.

Implementation detail:

Use a class named PaymentProviderClient.

ADRs should focus on meaningful decisions, not every naming choice।


Design Should Leave Room for Implementation

Too vague:

System will be clean and scalable.

Not useful।

Too detailed too early:

OrderService method line-by-line pseudocode
with every private method named.

also not ideal।

Good technical design lives in between:

responsibilities
flows
boundaries
state transitions
data ownership
transaction requirements
risks
major decisions

Implementation then fills in code detail।


Our Initial Design Direction

Based on what we know now, without finalizing later lessons, we can already state some high-level direction:

One Spring Boot backend application

with logical capability areas around:

Product

Inventory

Customer Context

Order

Payment

using:

PostgreSQL

for application-owned persistent state।

Core business workflows will be coordinated inside the application।

External payment communication will remain behind a separate integration boundary।

Authenticated identity will enter from the existing identity capability।

Important local multi-state mutations will use transactional consistency where applicable।

This is direction, not final architecture।


What We Still Need to Determine

Before writing the RFC, we still need to answer:

Which domain concepts are entities?

Which are value objects?

What exactly does Order own?

Should Inventory be separate from Product?

What exact responsibilities belong to each capability?

How should application-level components interact?

Where should persistence interfaces sit?

What is our initial code architecture?

Which technical decisions deserve ADRs?

These are the next lessons।


Technical Design Review Questions

A useful review checklist:

Requirements

Does the design satisfy every relevant acceptance criterion?

Scope

Does it avoid adding unrelated functionality?

Responsibilities

Is it clear which component owns which behaviour?

Data

Is data ownership clear?

Integrity

Are important invariants protected?

Transactions

Are multi-state operations consistent?

Failures

Are important failure modes recognized?

Security

Are authorization boundaries clear?

Dependencies

Are external systems isolated appropriately?

Maintainability

Can another engineer understand how the system works?

These questions matter more than whether the diagram looks sophisticated।


A Technical Design Is a Hypothesis

One useful mindset:

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

It is not sacred।

During implementation we may discover:

an assumption was wrong

a framework behaves differently

a query is inefficient

a boundary is awkward

a requirement was misunderstood

Then design can change।

Good engineering is not pretending uncertainty does not exist।

It is making decisions explicitly and revising them when evidence changes।


But Design Should Not Change Randomly

Revision should have reason।

For example:

Original design:
Inventory is updated using approach A.

New finding:
Approach A cannot safely handle required concurrent updates.

Decision:
Use approach B.

Reason:
Protect inventory invariant.

This is healthy design evolution।

Random:

I saw a blog post and rewrote everything.

is not।


Design and Backlog Influence Each Other

Our backlog contains:

BACKEND-110
Model Order and Order Item Domain

BACKEND-112
Implement Order Creation Workflow

Technical design may reveal ticket split or dependency needs to change।

For example, we may decide inventory concurrency support deserves explicit work।

Then backlog can be refined।

This does not mean previous backlog was wrong।

It means planning gets more precise as design gets more precise।


Do Not Turn the Course Into Architecture Theatre

Our project simulation should remain practical।

We will create:

RFC
ADR
system diagrams
tickets

only where they help engineering thinking।

We will not create documents solely to imitate a big company।

Every artifact needs a reason।

The final goal remains:

build and operate a correct backend

not:

produce the most documentation.

Engineering Principle

এই lesson-এর core principle:

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

আরেকটি important principle:

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


Where We Are Now

Our journey:

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

Technical design শুরু হয়েছে।

কিন্তু আমরা এখনও intentionally code-level detail-এ যাইনি।

Next step:

Identify the domain concepts
that make up our system.

Summary

এই lesson-এ আমরা শিখেছি:

  • Requirements বলে system কী করবে; technical design বলে কীভাবে করবে।
  • Technical design শুধু architecture diagram নয়।
  • Design requirements, constraints, risks এবং trade-offs থেকে derive হওয়া উচিত।
  • Framework-first design avoid করা উচিত।
  • Use cases responsibilities identify করতে সাহায্য করে।
  • Domain model database schema-এর আগে ভাবা useful।
  • Important business invariants design-এর core input।
  • Different rules different layers-এ enforce হতে পারে।
  • Order creation-এর মতো multi-state operations transaction boundary require করে।
  • External payment workflow local database transaction-এর চেয়ে fundamentally different।
  • One application-এর মধ্যেও clear logical boundaries দরকার।
  • Historical order price এবং current product price different facts।
  • Order lifecycle state transitions explicit করা উচিত।
  • API database CRUD mirror করা উচিত নয়; business behaviour expose করা উচিত।
  • Client-এর উপর business transaction orchestration ছেড়ে দেওয়া উচিত নয়।
  • Concurrency এবং failure modes implementation-এর আগে identify করা দরকার।
  • Technical design should state realistic risks and non-goals।
  • Design decisions should trace back to requirements।
  • We should choose minimum necessary complexity, not minimum thinking।
  • Design can evolve, but changes should be evidence-driven।

পরের lesson:

Identifying Domain Entities

সেখানে আমরা আমাদের requirements থেকে Customer, Product, Inventory, Order, Order Item এবং Payment analyse করব এবং বুঝব কোন concept কী represent করে, কোনটি entity, কোনটি value-like concept, তাদের identity ও lifecycle কী, এবং domain model-এ কোন relationship এবং invariant capture করা দরকার।