Designing the System
Defining System Responsibilities
আপনি একটি free preview lesson দেখছেন।
আগের lesson-এ আমরা আমাদের major domain concepts identify করেছি:
Product
Inventory
Customer Identity
Order
Order Item
Payment
এখন next question:
কোন responsibility কার?
একটি system-এ entities identify করা যথেষ্ট নয়।
যদি responsibility clear না থাকে, code খুব দ্রুত এমন হয়ে যেতে পারে:
Controller
↓
validation
business rules
database queries
payment HTTP calls
logging
state changes
error mapping
অথবা:
OrderService
নামের একটি giant class সবকিছু করতে শুরু করতে পারে।
Strong backend architecture-এর foundation হলো:
Each part of the system should have a clear reason to exist and a clear responsibility.
এই lesson-এ আমরা define করব:
- HTTP layer কী করবে
- application workflow কী করবে
- domain entities কী করবে
- persistence layer কী করবে
- external integration layer কী করবে
- security context কোথায় enter করবে
- কোন responsibility কোথায় রাখা উচিত নয়
Why Responsibility Boundaries Matter
ধরা যাক order creation endpoint:
POST /orders
একটি naive implementation এমন হতে পারে:
@PostMapping("/orders")
public OrderResponse createOrder(...) {
// validate request
// load customer
// query products
// check inventory
// calculate total
// reduce inventory
// create order
// save order
// map response
}
Technically এটা কাজ করতে পারে।
কিন্তু controller এখন:
- HTTP জানে
- database জানে
- domain rules জানে
- inventory rules জানে
- pricing জানে
- transaction workflow জানে
এটি responsibilities mix করছে।
As system grows, এই ধরনের code hard to test, review, reuse, and change হয়ে যায়।
A Useful High-Level Separation
আমাদের application-এ conceptually আমরা কয়েকটি responsibility boundary রাখব:
HTTP Layer
↓
Application Workflow
↓
Domain Model
↓
Persistence / External Integrations
আর security context HTTP/application boundary দিয়ে প্রবেশ করবে।
More concretely:
Controller
↓
Application Service / Use Case
↓
Domain Entities
↓
Repositories / Integration Ports
↓
PostgreSQL / Payment Provider
এটি কোনো rigid framework rule নয়।
এটি responsibility map।
HTTP Layer Responsibility
HTTP layer-এর কাজ:
HTTP request গ্রহণ করা
JSON input parse করা
transport-level validation করা
authenticated request context গ্রহণ করা
application workflow invoke করা
result HTTP response-এ map করা
known application/business errors
consistent API response-এ translate করা
HTTP layer-এর কাজ নয়:
order total calculate করা
inventory business rule decide করা
order lifecycle manage করা
database transaction orchestrate করা
payment provider protocol implement করা
Controller Should Speak HTTP
Controller primarily HTTP concerns নিয়ে কাজ করবে।
Example responsibilities:
POST /orders
Controller করবে:
request body গ্রহণ
request DTO validate
authenticated customer identity resolve
CreateOrder workflow call
result response DTO-তে map
Conceptually:
HTTP Request
↓
Controller
↓
CreateOrder
↓
HTTP Response
Controller should not become the place where business rules live।
Transport Validation vs Business Validation
এই distinction important।
Suppose request:
{
"items": null
}
এটি transport/input validation concern হতে পারে।
HTTP layer সহজেই reject করতে পারে।
Another request:
{
"items": [
{
"productId": 10,
"quantity": 5
}
]
}
Structurally valid।
কিন্তু database-এ available inventory:
3
এখন failure business-level।
So:
Transport validation
↓
Is the request structurally valid?
versus:
Business validation
↓
Is this operation allowed by current system state?
এগুলো আলাদা concerns।
Application Workflow Responsibility
Application layer use case coordinate করে।
For example:
Create Order
Cancel Order
Pay Order
Browse Products
Adjust Inventory
Application workflow জানে:
which domain concepts are involved
which repositories need to be called
which external capability is needed
which operation should happen in which order
where transactional coordination is required
But application workflow ideally low-level SQL বা provider HTTP format জানবে না।
Example: Create Order Workflow
Conceptually:
CreateOrder
↓
load products
↓
load/check inventory
↓
construct valid Order
↓
decrease inventory
↓
persist changes
Application workflow coordinates the process।
It may use:
ProductRepository
InventoryRepository
OrderRepository
but does not care whether those repositories internally use:
JPA
SQL
PostgreSQL driver
Application Layer Is Not "Put All Logic Here"
Another common mistake:
Controller thin করতে গিয়ে সবকিছু OrderService-এ ঢুকিয়ে দেওয়া।
Example:
class OrderService {
createOrder();
cancelOrder();
payOrder();
getOrders();
updateProduct();
adjustInventory();
}
This simply moves the giant class problem।
Application layer should still follow use-case and capability boundaries।
Use Case Thinking
Instead of one giant service:
CommerceService
think in behaviours:
CreateOrder
CancelOrder
PayOrder
GetCustomerOrders
Product side:
CreateProduct
UpdateProduct
DeactivateProduct
BrowseProducts
Inventory:
AdjustInventory
GetInventory
Again, this does not mean each must absolutely be a separate Java class।
The important part is responsibility clarity।
Domain Responsibility
Domain entities should protect rules about their own state।
For example, Order knows:
which lifecycle transition is valid
So conceptually:
order.cancel();
can enforce:
UNPAID → CANCELLED
and reject:
PAID → CANCELLED
Similarly:
order.markPaid();
can prevent:
CANCELLED → PAID
Domain Entities Should Not Coordinate Infrastructure
Order should not do:
paymentProvider.charge(...);
or:
orderRepository.save(this);
or:
inventoryRepository.restore(...);
Those are external/infrastructure or workflow responsibilities।
Domain entity should remain focused on business state and behaviour।
Product Responsibility
Product owns current catalog-related state।
Conceptually:
Product
is responsible for:
current name / supported product data
current price
active/inactive ordering state
Behaviour may include:
deactivate
update supported product information
change price
Product does not own:
inventory quantity
orders containing the product
historical order-item prices
Why Product Does Not Own Historical Price
Current Product price:
€25
Historical Order Item price:
€20
These represent different business facts।
Therefore Product should not be used as the source of truth when displaying historical order price।
Order owns that historical meaning through its Order Items।
Inventory Responsibility
Inventory owns current quantity-related state।
It should protect:
quantity cannot become negative
and provide meaningful behaviour such as:
canFulfill(quantity)
decrease(quantity)
increase(quantity)
adjust(...)
Exact Java API later।
Inventory should not know:
HTTP requests
OrderRepository
Customer ownership
Payment provider
Domain Rule vs Concurrency Protection
Inventory can enforce:
currentQuantity - requestedQuantity >= 0
inside its object model।
But this alone does not protect against concurrent database transactions।
For example:
Request A reads 1
Request B reads 1
Both may locally pass validation।
Therefore responsibility is split:
Inventory domain
↓
protects quantity rules
and:
Persistence / transaction strategy
↓
protects those rules under concurrency
This is a critical distinction।
Order Responsibility
Order owns:
order identity
customer ownership reference
order lifecycle
order items
historical item pricing
order-level consistency
Important rules:
order has at least one item
order items remain part of order history
paid order cannot be cancelled in v1
cancelled order cannot become paid
Order should not know:
how Product is queried
how Inventory is stored
how HTTP authentication works
how Payment Provider API works
Order Item Responsibility
Order Item is part of Order।
It represents:
product reference
quantity
purchase-time unit price
It should maintain its own basic validity:
quantity > 0
price is valid
But Order owns the collection and resulting order-level consistency।
Who Calculates the Order Total?
This is an important responsibility question।
The total is defined by:
Order Items
So conceptually:
Order
is the natural owner of order total consistency।
Rather than controller calculating:
BigDecimal total = ...
and then injecting it into Order।
Better domain direction:
Order is created from valid items
↓
Order determines/maintains its total
The exact implementation may calculate during construction or derive on demand।
But ownership belongs with Order।
Who Determines Product Price?
Current catalog price belongs to Product।
During order creation:
CreateOrder workflow
loads relevant Products।
It uses current Product price to construct historical Order Items।
Conceptually:
Product.currentPrice
↓
CreateOrder workflow
↓
OrderItem.purchaseTimePrice
After that, historical price belongs to Order।
Who Checks Whether Product Can Be Ordered?
There are two parts:
Product state
and:
Inventory availability
Product can tell:
Am I active/orderable?
Inventory can tell:
Can I satisfy quantity 3?
CreateOrder workflow coordinates both।
This is cleaner than creating ambiguous:
Product.available
which mixes unrelated concerns।
Customer Context Responsibility
Authentication comes from external identity capability।
Our application needs something like:
AuthenticatedUser
or:
CurrentUser
containing application-relevant identity information।
For customer use cases:
CustomerId
is important।
For admin operations:
administrator authority / role
is important।
Security Layer vs Ownership Rules
Suppose customer accesses:
GET /orders/5001
Security layer can establish:
request is authenticated
But application must still ensure:
Order #5001 belongs to this customer.
These are different responsibilities।
Authentication:
Who are you?
Authorization/ownership:
Are you allowed to operate on this resource?
Why Controller Annotation Alone May Not Be Enough
An annotation might protect:
only authenticated customers can call endpoint
But it may not know:
which customer owns Order #5001
Ownership depends on domain/application data।
Therefore customer-scoped use cases need ownership-aware logic।
Payment Responsibility
Payment has two distinct sides।
Application-facing side
Our system needs something like:
request payment
receive outcome
Application should reason in business terms:
success
failure
provider reference
without dealing with provider-specific JSON everywhere।
Provider-facing side
Integration layer knows:
provider URL
authentication headers
request schema
response schema
HTTP status codes
timeout configuration
provider-specific error representation
Conceptually:
PayOrder Workflow
↓
Payment Gateway
↓
Payment Provider Client
↓
External Provider
Why Payment Provider Details Must Stay Isolated
Without separation:
PayOrderService
might contain:
business rules
JSON serialization
HTTP headers
provider error codes
order state transitions
database operations
Then changing provider protocol can disturb core order logic।
Isolation reduces coupling।
But Avoid Over-Abstraction
We have one provider।
We do not need:
PaymentProviderFactory
PaymentProviderRegistry
AbstractPaymentProcessor
PaymentStrategyResolver
just in case।
A simple boundary is enough:
PaymentGateway
with one implementation।
The boundary exists because responsibilities differ, not because multiple providers exist।
Persistence Responsibility
Persistence layer is responsible for storing and retrieving application-owned state।
Conceptually:
ProductRepository
InventoryRepository
OrderRepository
These repositories expose operations application workflows need।
Examples:
find product by id
find inventory for product
save order
find order by id
find orders belonging to customer
Repository Should Not Contain Business Workflow
Bad:
orderRepository.createOrderAndDecreaseInventoryAndValidateProducts(...);
This hides a business workflow inside persistence code।
Repository should primarily persist/retrieve state।
Application workflow should coordinate business operations।
But Repository Can Express Persistence-Specific Atomic Operations
There is nuance।
Concurrency-safe inventory updates may require a persistence operation such as:
load inventory with required locking semantics
or:
atomic conditional update
That belongs to persistence because it is about enforcing correctness under database concurrency।
Application workflow invokes it without knowing low-level SQL details।
So repository abstraction should not be artificially limited to:
save()
findById()
if current persistence requirements need stronger semantics।
Database Responsibility
PostgreSQL is not just passive storage।
It can protect important persisted invariants through:
NOT NULL
FOREIGN KEY
UNIQUE
CHECK constraints
transactions
For example:
inventory.quantity >= 0
may be reinforced by a database CHECK constraint।
But database should not become the only place where business behaviour exists।
Multiple Layers Can Protect Important Rules
Suppose quantity must be positive।
Possible protection:
HTTP input validation
↓
reject malformed/obviously invalid input
Domain model
↓
prevent invalid business state
Database constraint
↓
prevent invalid persisted state
This may look like duplication, but each boundary provides different protection।
We should use it selectively for important invariants।
Transaction Responsibility
Transaction boundary generally belongs around application workflows that must complete atomically।
Example:
Create Order
requires:
decrease inventory
persist order
persist items
All-or-nothing requirement means these local changes should be one transaction।
Conceptually:
CreateOrder
↓
transaction begins
↓
validate / modify local state
↓
persist
↓
commit
Failure:
rollback
Domain Entity Should Not Begin Transactions
We do not want:
order.beginTransaction();
Transaction is infrastructure/application coordination concern।
Order knows valid business state।
Application workflow defines business operation boundary।
Persistence framework implements transaction mechanics।
Cancellation Transaction Boundary
Cancellation changes:
Order status
+
Inventory quantity
Requirement says both must stay consistent।
Therefore:
CancelOrder
is one local transactional workflow।
If inventory restore fails:
Order should not remain CANCELLED
with inventory unchanged।
This requirement influences transaction scope।
Payment Transaction Boundary Is Different
Payment crosses:
PostgreSQL
+
External Payment Provider
We cannot put external payment provider inside a true PostgreSQL atomic transaction।
So:
@Transaction
cannot magically make external side effects rollback।
This is why payment workflow needs careful failure and idempotency design later।
Important responsibility rule:
Local transaction mechanisms protect local database consistency; external integration consistency needs additional design.
Error Responsibility
Different layers handle different kinds of error.
HTTP Layer
Responsible for mapping application outcomes to API responses।
Examples:
400
404
409
401 / 403
5xx
Exact contract later।
Application Layer
Understands operation-level failures such as:
OrderNotFound
InsufficientInventory
OrderNotCancellable
OrderNotPayable
Names are illustrative; exact exception/result model later।
Domain Layer
Protects invalid state transitions and invariants।
Example:
cannot cancel paid order
Integration Layer
Translates provider-specific errors into application-level outcomes।
Example:
Provider returned code X42
should not leak throughout business code।
Integration layer may translate it into:
PaymentRejected
or another application concept।
Avoid Provider Errors Leaking Everywhere
Bad:
if (providerResponse.getCode().equals("X42")) {
...
}
inside Order logic।
Provider code X42 is external protocol detail।
Only integration layer should understand it।
Logging Responsibility
Logging is cross-cutting, but not every layer should log everything।
Bad:
Controller logs request
Service logs same request
Repository logs same request
Domain logs same request
This creates noise।
We will design logging later, but basic principle:
- log where operational context matters
- avoid duplicating the same event at every layer
- domain logic should not depend on logging infrastructure
Mapping Responsibility
We will likely have different representations:
HTTP Request DTO
Domain objects
Persistence representation
HTTP Response DTO
Not every transition necessarily requires a dedicated mapper class।
But responsibility should be clear।
Controller/application boundary handles transport mapping।
Persistence layer handles storage representation concerns।
Domain should not know JSON fields।
Do We Need Separate Persistence Models?
Not decided yet।
Two valid approaches:
Simpler
Domain object
=
JPA entity
Less mapping overhead।
More isolated
Domain model
≠
Persistence model
Stronger persistence separation but more mapping complexity।
We will choose based on our project's size and needs in the architecture lesson।
We should not create extra layers just to appear "clean architecture."
Responsibility Does Not Require a Class per Responsibility
This is important।
If we say:
HTTP mapping responsibility
it does not automatically mean:
OrderRequestMapperFactory
must exist।
If we say:
payment boundary
it does not require five interfaces।
Architecture should express responsibility with the least complexity necessary।
Product Capability Responsibility Map
Let's summarize Product.
Product Domain
Owns:
current product state
current price
active/inactive behaviour
Product Application Workflow
Coordinates:
create product
update product
deactivate product
browse products
Product Persistence
Owns:
loading and storing products
Product HTTP Layer
Owns:
REST request/response handling
Inventory Capability Responsibility Map
Inventory Domain
Owns:
current quantity
quantity validity
increase/decrease behaviour
Inventory Application Workflow
Coordinates:
administrator adjustment
inventory retrieval
cross-domain interaction during ordering
Inventory Persistence
Owns:
retrieval
storage
concurrency-safe access/update strategy
Order Capability Responsibility Map
Order Domain
Owns:
order state
order items
order total consistency
valid lifecycle transitions
Order Application Workflows
Own:
Create Order
View Order History
Cancel Order
coordinate Order with Product and Inventory
Order Persistence
Owns:
store/retrieve orders
customer-scoped order queries
Order HTTP Layer
Owns:
order REST API
Payment Responsibility Map
Order Domain
Knows:
whether order state allows payment
how to transition to PAID
Payment Workflow
Coordinates:
ownership
eligibility
provider invocation
applying confirmed result
Payment Integration
Owns:
external provider protocol
Payment Persistence
Exact requirement:
deferred until payment workflow design
because we have not yet determined what payment-attempt data must be persisted।
Customer Context Responsibility Map
External Identity System
Owns:
authentication
identity lifecycle
Security Integration
Owns:
extract authenticated identity
map external identity to application context
Application Workflows
Own:
customer ownership enforcement
Domain
Order stores customer ownership reference but does not authenticate users।
Example: Create Order End-to-End
Let's map responsibilities clearly.
Client
↓
HTTP Controller
Controller:
parse request
transport validation
get authenticated customer
Then:
CreateOrder Workflow
Workflow:
load Products
load Inventory
validate cross-concept availability
construct Order
update Inventory
persist changes
Domain:
Product
→ current price / orderable state
Inventory
→ quantity rules
Order
→ item/lifecycle/total rules
Persistence:
Repositories
→ PostgreSQL
Then result returns upward:
Order
↓
Response mapping
↓
HTTP response
Example: Cancel Order End-to-End
Client
↓
Controller
↓
CancelOrder Workflow
Workflow:
load Order
verify ownership
Order.cancel()
restore Inventory
persist atomically
Domain responsibilities:
Order
→ whether cancellation is valid
Inventory
→ valid quantity increase
Application responsibility:
coordinate both
Database responsibility:
commit both together
Example: Pay Order End-to-End
Client
↓
Controller
↓
PayOrder Workflow
Workflow:
load Order
verify ownership
verify Order can be paid
invoke PaymentGateway
Payment integration:
translate request to provider API
call provider
translate provider outcome
Back in workflow:
if confirmed success
apply payment result safely
mark Order paid
Exact failure semantics later।
What Should Not Depend on What?
Dependency direction is important।
Bad:
Order
↓
OrderController
Domain should never depend on HTTP layer।
Bad:
Order
↓
JpaRepository
Domain should ideally not depend directly on persistence framework details।
Bad:
Order
↓
PaymentProviderHttpResponse
Domain should not depend on provider protocol।
Better:
HTTP
↓
Application
↓
Domain
and infrastructure supports application boundaries।
Framework Should Stay at the Edges Where Practical
Spring Boot will naturally appear in:
controllers
configuration
security
transactions
repositories
HTTP clients
But core business concepts such as:
Order
Inventory
Product
should not require Spring annotations merely to express business behaviour।
This makes the domain easier to understand and test।
Does This Mean "Pure Clean Architecture"?
No।
We are not committing to a heavyweight Clean Architecture implementation with dozens of adapters and interfaces।
Our goal is practical separation।
We will use Spring Boot naturally।
We simply do not want:
business behaviour
to become inseparable from:
HTTP
JPA
external provider protocol
unless separation adds no real value।
Avoid Interface-For-Everything
A common overengineering pattern:
ProductService
ProductServiceImpl
OrderService
OrderServiceImpl
InventoryService
InventoryServiceImpl
with only one implementation and no meaningful boundary reason।
Interfaces are useful when they represent:
a genuine abstraction boundary
external dependency boundary
multiple implementations
testing seam where appropriate
Not because "Spring projects should have interfaces."
Payment Gateway Is a Meaningful Interface Candidate
Payment integration is an example where a boundary is meaningful।
Application wants:
process payment
External implementation uses:
HTTP provider
This boundary separates two different responsibilities।
Therefore interface/port-like abstraction can be justified।
Repository Is Also a Meaningful Boundary
Application wants:
load Order
save Order
It should not care about:
SQL
JPA EntityManager
database connection
Repository represents persistence capability।
Again, exact repository design later।
Domain Entity Does Not Need an Interface
Creating:
Order
OrderImpl
has no clear value।
Order is a concrete domain concept।
Not every class needs abstraction।
Avoid Generic Service Names Where Possible
Names like:
OrderManager
OrderHandler
OrderProcessor
OrderHelper
often hide responsibility।
Prefer behaviour-oriented names where useful:
CreateOrder
CancelOrder
PayOrder
or a cohesive:
OrderApplicationService
if several closely related use cases remain understandable together।
Naming should communicate responsibility।
Responsibility and Package Structure
Later package structure may reflect capabilities rather than only technical layers।
For example conceptually:
product/
inventory/
order/
payment/
security/
Within each capability we can organize HTTP/application/domain/persistence responsibilities sensibly।
Alternative layer-first structure:
controller/
service/
repository/
entity/
can work for small applications, but as system grows related feature code becomes scattered।
We will decide exact structure in the next architecture lesson।
Capability-Oriented Thinking
Suppose engineer works on cancellation।
With capability-oriented organization, relevant code may mostly live around:
order/
instead of navigating:
controllers/OrderController
services/OrderService
entities/Order
repositories/OrderRepository
dto/OrderResponse
across the entire repository।
This can improve local reasoning।
But again, we should not create unnecessary nested package complexity।
Responsibility Boundaries Improve Testing
If Order lifecycle rule lives inside Order:
PAID cannot cancel
we can test that without Spring or PostgreSQL।
If CreateOrder workflow coordinates repositories through clear boundaries, we can test workflow behaviour separately।
If repository handles PostgreSQL, integration tests can focus on persistence।
If controller handles HTTP, API tests can focus on HTTP contract।
This creates meaningful test levels।
Testing Responsibility Map
Conceptually:
Domain Test
→ business state and rules
Application Test
→ workflow coordination
Persistence Integration Test
→ database mapping/query behaviour
API Integration Test
→ HTTP contract and end-to-end behaviour
We will formalize testing later।
Responsibility Boundaries Improve Changeability
Suppose payment provider changes its request format।
Good boundary:
Payment Provider Client
changes।
Order lifecycle code ideally remains unchanged।
Suppose API response format changes।
Controller/DTO mapping changes।
Order domain remains unchanged।
Suppose inventory concurrency strategy changes।
Persistence implementation changes।
CreateOrder business intent stays largely stable।
This is the practical value of separation।
But Boundaries Have Cost
Every abstraction introduces:
more files
more names
more indirection
more code to navigate
Therefore we need balance।
Our question is not:
How many layers can we create?
It is:
Which boundaries protect meaningful differences in responsibility?
Our Responsibility Design Principles
For this project we will follow these principles:
1. Controllers handle HTTP, not business workflows.
2. Application workflows coordinate use cases.
3. Domain entities protect their own business state.
4. Repositories own persistence interaction.
5. External provider protocol stays inside integration code.
6. Authentication infrastructure provides identity context.
7. Application workflows enforce resource ownership.
8. Transaction boundaries surround coherent business operations.
9. Domain objects do not call repositories or external services.
10. Abstractions are introduced only when they clarify a real boundary.
What About Business Rules In Repositories?
Avoid rules like:
if order is paid, don't cancel
inside repository implementation।
Repository should not determine domain lifecycle policy।
But it may enforce persistence mechanics required by business correctness, such as:
concurrency-safe inventory access
This distinction matters।
What About Business Rules In Controllers?
Controller may validate:
required field missing
but should not decide:
paid order cannot cancel
because same use case could later be triggered from another transport or internal workflow।
Business rule belongs deeper।
What About Business Rules In Database?
Database constraints can reinforce state integrity।
But we should not put all domain behaviour into triggers and stored procedures while Java application merely passes data around।
Our application owns business logic।
PostgreSQL protects important persisted invariants and transaction consistency।
What About Business Rules In DTOs?
DTO can use simple validation:
required
positive
But DTO should not become a domain entity।
Example:
CreateOrderRequest
represents incoming transport data।
Order represents business state।
They have different responsibilities।
Cross-Cutting Concerns
Some responsibilities cut across features:
logging
security
error handling
configuration
metrics
These should be implemented consistently without polluting domain concepts।
For example:
Order
should not know Micrometer metrics APIs।
Payment provider client may emit operational metrics later, but domain remains independent।
What We Deliberately Avoid
We will not introduce:
Generic BaseService
Generic CRUD Repository abstraction above Spring Data
Universal Mapper framework
Event Bus
Command Bus
Mediator framework
Plugin architecture
unless later requirements create a clear need।
These would add architecture before problem justification।
Current Responsibility Map
Our system can now be summarized like this:
┌──────────────────────────────┐
│ HTTP / Security Boundary │
│ │
│ Controllers │
│ Request/Response DTOs │
│ Authenticated identity │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ Application Workflows │
│ │
│ Create Order │
│ Cancel Order │
│ Pay Order │
│ Browse Products │
│ Manage Product │
│ Manage Inventory │
└───────┬───────────┬──────────┘
│ │
▼ ▼
┌─────────────┐ ┌────────────────┐
│ Domain │ │ External │
│ │ │ Integration │
│ Product │ │ │
│ Inventory │ │ Payment Gateway│
│ Order │ └───────┬────────┘
│ Order Item │ │
└──────┬──────┘ ▼
│ Payment Provider
▼
┌──────────────────────────────┐
│ Persistence │
│ │
│ Product Repository │
│ Inventory Repository │
│ Order Repository │
└──────────────┬───────────────┘
│
▼
PostgreSQL
This is still conceptual।
Exact Java packages/classes come next।
Checking the Design Against Our Main Workflows
Browse Products
Controller
↓
BrowseProducts workflow
↓
Product + Inventory persistence
↓
response
No Order involvement required।
Create Order
Controller
↓
CreateOrder workflow
↓
Product
Inventory
Order
Repositories
↓
transaction
Clear responsibility split।
Cancel Order
Controller
↓
CancelOrder workflow
↓
Order.cancel()
Inventory.increase(...)
Repositories
↓
transaction
Clear।
Pay Order
Controller
↓
PayOrder workflow
↓
Order eligibility
Payment Gateway
↓
Provider Client
↓
External Provider
Then confirmed success updates local Order state safely।
Clear conceptually, with failure mechanics still to be designed later।
Responsibility Smells
Some warning signs we should watch for during implementation।
Giant Controller
Controller performs business workflow.
Bad sign।
Giant Service
One service owns products, orders, payments, inventory.
Bad sign।
Infrastructure-Aware Domain
Order imports Spring WebClient or JpaRepository.
Bad sign।
Repository Business Logic
Repository decides whether Order can cancel.
Bad sign।
Provider Protocol Leakage
Order checks external provider status code.
Bad sign।
DTO as Domain Model
HTTP request object passed everywhere
and treated as business state.
Bad sign।
Responsibility Review Questions
When implementing a piece of logic, ask:
Does this logic concern HTTP?
Does it concern one domain object's state?
Does it coordinate multiple domain concepts?
Does it concern persistence mechanics?
Does it concern an external system protocol?
Does it concern authenticated identity or permissions?
The answer gives a strong clue about where the logic belongs।
Example: "Quantity Must Be Positive"
Where?
Potentially:
HTTP validation
+
Domain validation
because it is both an invalid external request and invalid business quantity।
Example: "Paid Order Cannot Be Cancelled"
Where?
Order domain behaviour
because it is an Order lifecycle invariant।
Application workflow calls:
order.cancel()
instead of duplicating the rule everywhere।
Example: "Customer Can Only Cancel Own Order"
Where?
CancelOrder application workflow
because it combines authenticated customer context with persisted Order ownership।
Example: "Inventory Update Must Be Concurrency-Safe"
Where?
Application transaction
+
Persistence strategy
because domain object alone cannot coordinate database concurrency।
Example: "Provider Code X42 Means Payment Rejected"
Where?
Payment provider integration
The application should receive a provider-independent outcome।
Example: "HTTP 409 Should Be Returned"
Where?
API error mapping
Application/domain should not need to know HTTP status numbers।
Responsibilities and Our Backlog
Our earlier tickets now have clearer implementation meaning।
For example:
BACKEND-112
Implement Order Creation Workflow
is primarily application-layer work coordinating:
Product
Inventory
Order
Repositories
while:
BACKEND-113
Expose Order Creation API
is HTTP-layer work।
This validates why we separated those tickets।
Responsibilities and Code Review
Reviewer can now ask:
Is this business rule in the right place?
Is controller doing too much?
Does domain depend on infrastructure?
Is provider-specific logic leaking upward?
Is persistence implementing business workflow?
Is ownership enforced at the application boundary?
Responsibility boundaries give code review an architectural vocabulary।
Responsibilities and Future Change
Later Module 14 introduces a production change request।
Clear responsibility boundaries mean we can ask:
Which domain rule changes?
Which workflow changes?
Does API contract change?
Does persistence change?
Does external integration change?
instead of touching everything blindly।
This is one reason we are establishing boundaries before implementation।
Engineering Principle
The core principle from this lesson:
Put behaviour where the responsibility naturally belongs, and keep coordination separate from the objects being coordinated.
More specifically:
Domain entities protect their own state. Application workflows coordinate use cases. Controllers speak HTTP. Repositories speak persistence. Integration code speaks external protocols.
And:
Do not add abstraction unless it protects a real responsibility boundary.
Our Current Responsibility Decisions
We will carry these decisions into the architecture lesson:
HTTP Layer
→ transport, API mapping, request context
Application Layer
→ use-case orchestration
→ ownership checks
→ transaction-level workflow coordination
Domain
→ Product, Inventory, Order, Order Item behaviour and invariants
Persistence
→ storing/retrieving state
→ database-specific concurrency mechanics
Payment Integration
→ provider protocol and translation
Security Integration
→ authenticated identity and authorities
Where We Are Now
Module 2:
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
We now understand:
what the important concepts are
and:
who should be responsible for what
Next we can combine those decisions into the application’s initial architecture।
Summary
In this lesson, we established that:
- Responsibility boundaries prevent controllers and services from becoming giant mixed components.
- HTTP layer handles transport concerns, not business workflows.
- Application workflows coordinate use cases and multiple domain concepts.
- Domain entities protect rules about their own state.
- Product owns current product state and price.
- Inventory owns quantity and inventory rules.
- Order owns lifecycle, items, historical meaning, and order-level consistency.
- Order Item belongs to Order.
- Customer authentication is externally owned, while our application enforces order ownership.
- Payment provider details stay inside the integration layer.
- Repositories own persistence interaction, not business lifecycle decisions.
- Database transactions protect local multi-state workflow consistency.
- Domain validation alone cannot solve database concurrency.
- Payment cannot be made atomic with PostgreSQL simply by using a local transaction because the provider is external.
- API errors, domain errors, and provider errors belong to different boundaries.
- Interfaces are not required everywhere; they should represent meaningful boundaries.
- Payment integration and repositories are legitimate abstraction boundaries.
- Domain entities should not depend on HTTP, repositories, JPA details, or provider protocols.
- We will use practical separation rather than a heavyweight architecture pattern for its own sake.
Next lesson:
Designing the Initial Architecture
There we will turn these responsibility decisions into the first concrete application architecture, including package/module boundaries, dependency direction, request flow, persistence placement, transaction boundaries, and the structure we will later implement in Spring Boot.