Designing the System

Defining System Responsibilities

ReadingPreview

আপনি একটি 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.