Designing the System
From Requirements to Technical Design
আপনি একটি 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 করা দরকার।