Designing the System

Designing the Initial Architecture

ReadingPreview

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

এখন পর্যন্ত আমরা তিনটি গুরুত্বপূর্ণ design step complete করেছি।

প্রথমে requirements থেকে technical design-এর দিকে গিয়েছি।

তারপর major domain concepts identify করেছি:

Product
Inventory
Customer Identity
Order
Order Item
Payment

এরপর responsibility boundaries establish করেছি:

HTTP
Application Workflows
Domain
Persistence
Security Integration
External Payment Integration

এখন আমাদের next responsibility:

এই concepts এবং responsibilities বাস্তব Java/Spring Boot application-এর মধ্যে কীভাবে organize করা হবে?

এই lesson-এ আমরা Order Management Backend-এর initial architecture design করব।

আমরা decide করব:

  • application-এর major capability boundaries
  • source-code organization
  • dependency direction
  • HTTP request কীভাবে system-এর মধ্যে flow করবে
  • domain logic কোথায় থাকবে
  • repositories কোথায় থাকবে
  • transaction boundary কোথায় থাকবে
  • Spring Boot framework কোন জায়গায় ব্যবহার হবে
  • payment integration কীভাবে isolate হবে
  • কোন architecture patterns আমরা intentionally ব্যবহার করব না

এই architecture পরে আমাদের RFC-এর foundation হবে।


Architecture Is the Shape of Responsibilities

Architecture মানে শুধু:

Controller
Service
Repository

এই তিনটি box নয়।

Architecture মূলত define করে:

What are the major parts of the system?

What is each part responsible for?

How can those parts depend on each other?

How does data and control flow through the system?

Where are important boundaries?

একটি ভালো architecture developer-কে guide করে:

নতুন logic কোথায় রাখা উচিত?

একটি দুর্বল architecture-এ answer হয়:

Wherever it is easiest right now.

এবং কিছুদিন পর codebase unpredictable হয়ে যায়।


Our Constraints Are Already Known

আমরা architecture completely freely invent করছি না।

Engineering context already establish করেছে:

Java

Spring Boot

Gradle

REST / JSON

PostgreSQL

Flyway

Existing identity capability

External payment provider

Containerized deployment

আর current system:

one deployable backend application

হবে।

Therefore আমরা microservices architecture design করছি না।


Architecture Goal

আমাদের architecture-এর goal:

Simple enough to understand
        +
Structured enough to evolve
        +
Clear enough to test
        +
Safe enough for important business rules

We do not want:

maximum abstraction

or:

minimum number of files

We want appropriate structure।


One Application, Multiple Capabilities

Our backend is one deployable Spring Boot application।

Internally, however, it has several meaningful capabilities:

Product
Inventory
Order
Payment
Security / Customer Context

Conceptually:

Order Management Backend

├── Product
├── Inventory
├── Order
├── Payment
└── Security

This gives us logical modularity without distributed-system complexity।


Why Capability Boundaries Matter

Suppose everything is organized only by technical layer:

controller/
service/
repository/
entity/
dto/

Initially this looks simple।

But as system grows:

controller/
    ProductController
    InventoryController
    OrderController
    PaymentController

service/
    ProductService
    InventoryService
    OrderService
    PaymentService

repository/
    ProductRepository
    InventoryRepository
    OrderRepository

A developer working on Order cancellation must jump across many unrelated top-level directories।

The code is organized by how it is implemented, not what capability it belongs to


Capability-Oriented Structure

Instead, we can keep code close to the business capability it supports।

Conceptually:

product/
inventory/
order/
payment/
security/

Inside each capability, we can separate responsibilities as needed।

For example:

order/
├── api/
├── application/
├── domain/
└── persistence/

This keeps Order-related code near other Order-related code।


Do We Need Four Subpackages Everywhere?

No।

We should not create empty directory hierarchies just to make architecture look sophisticated।

For a small capability, perhaps:

product/
├── Product
├── ProductRepository
├── ProductApplicationService
└── ProductController

might initially be enough।

As the capability grows, subpackages become useful।

The architecture principle is:

Organize around capability first, then separate responsibilities where complexity justifies it.


Our Initial Package Direction

A reasonable starting structure:

src/main/java/.../

├── product/
├── inventory/
├── order/
├── payment/
├── security/
└── shared/

shared/ must remain small।

It should not become:

everything that we don't know where to put

Avoid the Giant common Package

A common codebase smell:

common/
├── BaseEntity
├── StringUtils
├── DateUtils
├── OrderHelper
├── PaymentHelper
├── CommonService
└── MiscUtils

This usually means boundaries are breaking down।

A shared package should contain genuinely cross-cutting concepts only when needed।

For example:

shared/
    error/

might make sense if API error handling is application-wide।

But:

OrderCalculationHelper

belongs with Order।


Possible Order Package

Order is our most behaviour-rich capability।

A more structured package could eventually look like:

order/
├── api/
│   ├── OrderController
│   ├── CreateOrderRequest
│   └── OrderResponse
│
├── application/
│   ├── CreateOrder
│   ├── CancelOrder
│   └── GetCustomerOrders
│
├── domain/
│   ├── Order
│   ├── OrderItem
│   └── OrderStatus
│
└── persistence/
    ├── OrderRepository
    └── ...

Exact class names are not final।

The important part is responsibility separation।


Product Package

Product may initially be simpler:

product/
├── api/
├── application/
├── domain/
└── persistence/

Responsibilities:

api
→ product REST operations

application
→ create/update/deactivate/browse workflows

domain
→ Product behaviour

persistence
→ Product storage

Inventory Package

Inventory:

inventory/
├── api/
├── application/
├── domain/
└── persistence/

But some inventory operations are internal to order workflows।

Therefore not every inventory capability needs a public API।

For example:

decrease inventory during order creation

is internal application behaviour।

While:

administrator adjusts inventory

has an API।


Payment Package

Payment requires stronger external-boundary separation։

Conceptually:

payment/
├── application/
├── domain/
└── integration/

Potentially:

payment/
├── application/
│   └── PayOrder
│
├── domain/
│   └── PaymentResult
│
└── integration/
    ├── PaymentGateway
    └── ProviderPaymentClient

Exact persistent payment model remains deferred।


Security Package

Security is different from business capabilities।

It may contain:

security/
├── SecurityConfiguration
├── AuthenticatedUser
└── CurrentUserProvider

or similar concepts।

The goal:

Spring Security details

should not leak throughout domain classes।

Application workflows should receive application-relevant identity context।


Architecture Layers Within Capabilities

We are effectively using four responsibility types:

API
Application
Domain
Infrastructure

Where Infrastructure may include:

Persistence
External Integration
Framework Configuration

Conceptually:

API
 ↓
Application
 ↓
Domain

Application
 ↓
Persistence

Application
 ↓
External Integration

This is more useful than one global layered directory structure।


Dependency Direction

A critical architecture rule:

API
    depends on
Application
Application
    depends on
Domain

Application may also depend on abstract persistence/integration capabilities।

But:

Domain

should not depend on:

Controller
Spring MVC
JPA repository implementation
WebClient
Payment provider JSON

Domain Is the Most Stable Core

Framework details change more easily than core business rules।

For example:

Paid order cannot be cancelled.

This rule remains true whether API uses:

REST

or:

GraphQL

And whether persistence uses:

JPA

or direct SQL।

So business behaviour should not be tightly coupled to transport or persistence details।


Does Domain Need Zero Framework Annotations?

Ideally domain behaviour should remain understandable without framework knowledge।

But we should be pragmatic।

If later we decide to use the same class as domain model and JPA entity, there may be JPA annotations on domain classes।

That is not automatically bad।

We are not pursuing theoretical purity।

The important question:

Does framework coupling make business behaviour significantly harder to understand, test, or evolve?

If no, simpler implementation may be better।


Architecture Purity vs Practicality

Two extremes:

Extreme 1 — Everything Mixed

Controller
does everything.

Poor separation।

Extreme 2 — Maximum Clean Architecture Ceremony

20 interfaces

separate model for every layer

mapper for every object

adapter for every method

before the system needs it।

Also poor for our context।

Our architecture should sit between them।


Initial Architectural Style

We can describe our architecture as:

A modular monolithic Spring Boot application organized around business capabilities, with clear separation between HTTP, application workflows, domain behaviour, persistence, and external integrations.

Important words:

modular
monolithic
capability-oriented

What Does "Modular Monolith" Mean Here?

It means:

one application
one deployment unit

but internally:

clear logical modules/capabilities

This is not necessarily Java Platform Module System।

It is an architectural organization approach।

We are not introducing:

module-info.java

unless later required।


Why a Modular Monolith Fits Our Current Requirements

Our core workflows need strong local consistency।

For example:

Create Order
    ↓
Order
+
Inventory

Because they exist inside one application and PostgreSQL-backed system, we can use one database transaction where appropriate।

This avoids distributed transaction complexity։

For our current scale and requirements, this is a strong advantage।


Microservices Would Add Problems We Do Not Need

If Product, Inventory, and Order were separate services:

Order Service
    ↓ HTTP
Inventory Service

Order creation suddenly needs to reason about:

network failures

distributed consistency

partial success

retry semantics

service availability

cross-service tracing

Current requirements do not justify this cost।

So:

modular monolith

is deliberate simplicity।


Request Flow

Let's map a normal HTTP request.

Example:

POST /orders

Conceptually:

Client
   ↓
OrderController
   ↓
CreateOrder
   ↓
Product / Inventory / Order
   ↓
Repositories
   ↓
PostgreSQL

Response returns in reverse direction।


Detailed Create Order Flow

Conceptually:

HTTP Request
    ↓
OrderController

Controller:

parse JSON
validate basic structure
obtain authenticated customer identity

Then:

CreateOrder Application Workflow

Workflow:

load required Products
        ↓
verify Product orderability
        ↓
load relevant Inventory
        ↓
validate requested quantities
        ↓
construct Order Items with current prices
        ↓
construct Order
        ↓
decrease Inventory
        ↓
persist Order + Inventory

Transaction wraps the local state mutation։

Then:

Order
  ↓
map response
  ↓
HTTP response

Transaction Boundary Belongs Around the Use Case

Order creation is a coherent business operation।

Therefore transaction boundary conceptually belongs at:

CreateOrder

not separately around each repository call।

Bad:

save order transaction
commit

decrease inventory transaction
commit

If second fails, partial state।

Better:

CreateOrder transaction
    ↓
all required local database changes
    ↓
commit together

Spring @Transactional

Later implementation will likely use Spring transaction management।

Conceptually something like:

@Transactional
public Order createOrder(...) {
    ...
}

may represent the boundary।

But important lesson:

@Transactional is the implementation mechanism. The business operation defines the transaction boundary.

We do not begin with annotation and then decide what operation means।


Which Layer Owns @Transactional?

For our architecture, application workflow is the natural place।

Why?

It knows the complete business operation:

load
validate
change
persist

A repository only knows one persistence operation।

A domain entity should not know transaction infrastructure।

So application layer owns transaction coordination।


Persistence Architecture

Our persistence stack will eventually look roughly like:

Application Workflow
        ↓
Repository Boundary
        ↓
Spring Data / JPA implementation
        ↓
PostgreSQL

We still need to decide whether repository interface and Spring Data repository are the same thing or separately wrapped।


Should We Wrap Every Spring Data Repository?

Not necessarily।

Suppose:

interface ProductRepository extends JpaRepository<...>

is sufficient for Product application needs।

Adding:

ProductPersistencePort
ProductPersistenceAdapter
JpaProductRepository
ProductRepositoryImpl

might add unnecessary indirection।

But if domain/application need operations with meaningful business or concurrency semantics, a custom boundary may be justified।


Pragmatic Repository Design

Our rule:

Use the simplest repository abstraction that keeps persistence concerns from dominating application logic.

For straightforward Product CRUD, Spring Data repository may be enough।

For Inventory concurrency, we may need custom persistence behaviour।

For Order aggregate loading, repository may need domain-focused operations।

We do not force identical repository architecture across every capability।


Capability Consistency Does Not Mean Mechanical Uniformity

Bad architecture thinking:

Product has 4 layers
therefore Inventory must have exactly 4 layers
therefore Payment must have exactly 4 layers

Different capabilities have different complexity।

Product is mostly local CRUD/domain state।

Payment is external integration-heavy।

Order has workflow complexity।

Architecture should remain coherent, not mechanically symmetric।


Database Ownership

Our application owns one PostgreSQL persistence context for its domain data।

Conceptually:

products

inventory

orders

order_items

Payment-related tables may be added later if design requires them।

We are not splitting databases by capability।


Why One Database Is Appropriate

One application owns these concepts and needs local transactional consistency।

Using one PostgreSQL database allows:

Order creation transaction

to update Order and Inventory safely without distributed coordination।

Separate schemas/databases would create complexity with no current requirement benefit।


Database Table Design Comes Later

Architecture decides:

PostgreSQL owns application persistence

But we do not yet finalize:

column names

index definitions

foreign-key details

JPA relationships

Those belong in persistence implementation lessons।


Flyway Position

Flyway is infrastructure supporting persistence evolution।

Repository structure may include:

src/main/resources/db/migration/

with versioned migrations।

Conceptually:

Application code
+
Database migrations

evolve together।


API Architecture

API classes belong close to the capability they expose।

For example:

order/api/

could contain:

OrderController

CreateOrderRequest

OrderResponse

Transport DTOs should remain transport concerns।

They should not become domain objects simply because their fields look similar।


Request DTO vs Domain Object

Incoming request:

{
  "items": [
    {
      "productId": 101,
      "quantity": 2
    }
  ]
}

This represents:

what the client requested

It does not yet contain trusted:

price
order total

The application loads trusted Product data and creates domain Order Items।

Therefore:

CreateOrderRequest

and:

Order

must not be treated as the same concept।


Response DTO vs Domain Object

Similarly, API response might include:

id
status
items
total
createdAt

But domain may hold internal behaviour or fields not appropriate for public API।

Keeping mapping explicit prevents API contract from accidentally becoming the entire domain model।


Do We Need Mapper Classes?

Maybe not initially।

Simple conversion can happen in:

controller

or a small mapping function close to API boundary।

If mappings become complex or repetitive, dedicated mappers can be introduced।

We should not start with:

OrderRequestMapper
OrderResponseMapper
OrderItemMapper
ProductMapper

for trivial transformations unless they provide value।


External Payment Architecture

Payment crosses a system boundary։

Architecture:

PayOrder
    ↓
PaymentGateway
    ↓
ExternalPaymentClient
    ↓
Payment Provider

PayOrder should not know:

provider endpoint
provider authentication headers
provider-specific status codes
JSON field names

Payment Gateway Boundary

Application-facing abstraction may conceptually provide:

processPayment(...)

and return an application-defined result։

For example conceptually:

PaymentResult

representing:

successful
rejected

plus relevant provider reference/details required by the application।

Exact types later।


External Client Responsibility

The external client:

constructs provider request

performs HTTP call

handles timeout

reads provider response

maps provider-specific result

This is infrastructure/integration code।


Why Payment Client Is Not a Repository

A repository represents application-owned persisted state।

Payment provider is an external service।

Therefore calling it through something named:

PaymentRepository

would obscure responsibility।

Better language:

PaymentGateway

PaymentClient

or equivalent।

Naming should reflect boundary semantics।


Security Architecture

Spring Security will operate near the HTTP/application boundary।

Conceptually:

Incoming Request
      ↓
Spring Security
      ↓
Authenticated Identity
      ↓
Controller / Application Workflow

Domain objects should not parse:

JWT
Authorization header
security context

Current User Abstraction

We may expose application-friendly context:

AuthenticatedUser

containing something like:

userId
roles/authorities

Exact type later।

Customer workflows need customer identity।

Administrator workflows need authority check।


Where Authorization Happens

There are two broad levels.

Endpoint-Level

For example:

Only ADMIN can call product administration endpoint.

Spring Security can help enforce this।

Resource-Level

For example:

Customer can cancel only their own Order.

Application workflow must check ownership because it requires order data।

Both are needed।


Cross-Capability Dependencies

We need to control how capabilities depend on each other।

CreateOrder needs:

Product
Inventory
Order

Could Order application directly reach deeply into Product persistence implementation?

That would tightly couple capabilities।

Better to depend on the capability's relevant boundary or repository contract।

But we should keep this simple because all code is in one application।


Example Dependency

CreateOrder may need:

ProductRepository
InventoryRepository
OrderRepository

This is acceptable within the modular monolith if repository boundaries are explicit and ownership remains clear।

We do not need internal HTTP APIs between modules।

That would be unnecessary self-distribution।


No Internal HTTP Between Capabilities

Bad architecture:

Order module
   ↓ localhost HTTP
Product module

inside the same application।

This creates:

serialization
network-style errors
unnecessary latency
complexity

for no benefit।

Use normal Java calls inside the application।


No Internal Message Broker for Current Workflows

Similarly:

CreateOrder
    ↓
publish InventoryDecreaseRequested

and wait for another internal consumer is unnecessary for our required synchronous transaction।

Current requirement wants correct immediate order creation।

Direct application coordination is simpler and safer।


Dependency Cycles

We should avoid capability relationships such as:

Order depends on Inventory

Inventory depends on Order

in arbitrary directions।

Instead:

application workflow

coordinates both।

Domain capabilities should not mutually call each other without a clear reason।


Example: Cancellation

Bad:

Order.cancel()
   ↓
calls InventoryService

Now Order depends on Inventory।

Better:

CancelOrder
   ↓
Order.cancel()
   +
Inventory.increase(...)

Application workflow coordinates।

This prevents circular domain dependencies।


Product and Inventory Relationship

Inventory needs a product association।

But Product domain does not need to call Inventory。

For example:

Inventory
    references ProductId

or persistence relationship equivalent।

Product can remain independent of inventory mutation behaviour।

Browsing workflow may combine Product + Inventory when determining what to return।


Read Composition

A read use case may gather information from multiple capabilities।

Example:

Browse Available Products

needs:

Product
+
Inventory

Application workflow can compose the read result।

We don't need to merge Product and Inventory into one domain entity simply because one response needs both।


Write Ownership Matters More Than Read Composition

For architecture, ask:

Who owns mutation?

Product mutation:

Product capability

Inventory mutation:

Inventory capability

Order mutation:

Order capability

Reads can combine state more freely, but write ownership should remain clear।


Error Architecture

We need consistent error flow।

Conceptually:

Domain/Application Failure
        ↓
API Error Mapping
        ↓
HTTP Response

Examples:

ProductNotFound
InsufficientInventory
OrderNotCancellable
OrderNotPayable

Exact class names later।

HTTP layer maps them to consistent API representation।


Unexpected Failures

Unexpected failures such as:

database unavailable
unexpected exception

should not be converted into misleading business errors।

Architecture should distinguish:

expected domain/application outcomes

from:

unexpected operational failures

Later observability module will log/measure these appropriately।


Shared Error Handling

API error mapping is one legitimate cross-cutting concern։

We may eventually have:

shared/error/

or an application-wide exception handler։

For Spring MVC, something like:

@ControllerAdvice

can provide consistent response mapping।

But exact implementation comes in API module।


Configuration Architecture

Spring configuration should remain near infrastructure concerns।

For example:

payment provider base URL

timeout

database configuration

comes from application configuration/environment।

Domain classes should not inject:

@Value

for infrastructure settings।


Configuration Boundary

Conceptually:

Environment
    ↓
Spring Configuration
    ↓
Infrastructure Component

Example:

PAYMENT_PROVIDER_URL
    ↓
PaymentClientConfiguration
    ↓
ProviderPaymentClient

This keeps environment-specific values out of domain logic।


Time as a Dependency

Order history likely needs:

createdAt

One subtle architecture concern is current time।

Calling:

Instant.now()

everywhere works, but can make deterministic testing harder।

We do not need a complex time abstraction yet।

If time-sensitive behaviour later becomes significant, a Clock dependency may be useful।

For now, don't invent abstractions without need।


Money Architecture

Money belongs to domain values, but currency complexity is out of scope।

Architecture should ensure:

client cannot determine order price

Product provides current price

Order Item records purchase-time price

Order calculates total

Exact Java monetary type is an implementation detail to finalize later।


Transaction Architecture Summary

Local transactional workflows:

Create Order

inventory decrease
+
order persistence

must commit together।

Cancel Order

order cancellation
+
inventory restoration

must commit together।

Product CRUD:

single-domain updates

generally simpler transactions।

Payment:

external provider
+
local state

cannot be made one atomic PostgreSQL transaction।

This distinction is part of our architecture।


Concurrency Architecture

Inventory is the main current concurrency-sensitive state।

Requirement:

Two concurrent orders must not both consume
the same unavailable inventory.

Architecture therefore needs:

transaction boundary
+
database concurrency strategy

We have not chosen exact mechanism yet।

Possible persistence techniques will be evaluated later।

The architecture records the need, not a premature solution।


Why We Don't Choose Locking Yet

Possible options include:

pessimistic locking
optimistic concurrency
atomic conditional update

Choosing one now before persistence implementation detail would be premature।

Our RFC can identify the risk and desired property:

Inventory consumption must be concurrency-safe.

Then implementation design can document the chosen mechanism।


Architecture and Testing

Our structure naturally creates different test scopes।

Domain Tests

Test:

Order state transitions

Inventory quantity rules

Order total behaviour

without Spring where practical।


Application Workflow Tests

Test:

CreateOrder coordination

CancelOrder behaviour

PayOrder decisions

using controlled repository/integration dependencies।


Persistence Integration Tests

Test:

JPA mapping

queries

migrations

concurrency-sensitive persistence behaviour

against PostgreSQL through Testcontainers later।


API Integration Tests

Test:

HTTP request/response

validation

security

error mapping

This architecture supports the testing plan naturally।


Build-Time Dependency Rules

Could we enforce package dependencies automatically?

Tools exist for architecture testing, but current course does not require them।

We don't need to introduce architectural test frameworks immediately।

First establish clear structure and conventions।

If codebase grows enough to need automated enforcement later, that's a separate decision।


Spring Beans

Application workflows, repositories, controllers, and integration clients will often be Spring-managed beans।

For example:

Controller
@Service / component
Repository
@Configuration

But domain entities such as:

Order
Product
Inventory

should generally be ordinary objects created based on business state, not singleton Spring beans।


Domain Entity Is Not a Spring Service

We should never conceptually have one shared:

@Bean
Order

for all requests।

Order represents a specific business instance।

Spring beans represent application components/services/configuration।

Different concepts।


Dependency Injection

Spring Dependency Injection will connect application components।

For example:

CreateOrder

needs:

ProductRepository
InventoryRepository
OrderRepository

Rather than constructing dependencies manually inside each operation։

Conceptually:

Spring
  ↓
constructs CreateOrder
  ↓
provides required repositories

Module 3 will teach this practically।


Constructor Injection

Our implementation will generally prefer constructor injection for required dependencies।

Why?

Because dependency is explicit।

Conceptually:

CreateOrder(
    ProductRepository productRepository,
    InventoryRepository inventoryRepository,
    OrderRepository orderRepository
)

makes requirements visible।

We won't rely on hidden global service lookup patterns।

Exact code comes later।


Architecture Should Make Dependencies Visible

A class needing ten unrelated dependencies may be telling us:

too many responsibilities

This is another benefit of constructor-based explicit dependency design։

Architecture problems become easier to see।


Initial Architecture Diagram

Our current design can be represented like this:

                    ┌─────────────────────┐
                    │ Existing Identity   │
                    │ Capability          │
                    └──────────┬──────────┘
                               │
                               ▼
┌────────────┐        ┌───────────────────────┐
│   Client   │───────▶│ HTTP / Security       │
└────────────┘        │ Controllers + DTOs    │
                      └───────────┬───────────┘
                                  │
                                  ▼
                      ┌───────────────────────┐
                      │ Application Workflows │
                      │                       │
                      │ CreateOrder           │
                      │ CancelOrder           │
                      │ PayOrder              │
                      │ BrowseProducts        │
                      │ ManageInventory       │
                      └───────┬─────────┬─────┘
                              │         │
                    ┌─────────┘         └───────────┐
                    ▼                               ▼
          ┌──────────────────┐            ┌──────────────────┐
          │ Domain           │            │ Payment          │
          │                  │            │ Integration      │
          │ Product          │            │                  │
          │ Inventory        │            │ PaymentGateway   │
          │ Order            │            │ Provider Client  │
          │ Order Item       │            └────────┬─────────┘
          └─────────┬────────┘                     │
                    │                              ▼
                    │                    External Payment Provider
                    ▼
          ┌──────────────────┐
          │ Persistence      │
          │                  │
          │ Repositories     │
          │ JPA / PostgreSQL │
          └─────────┬────────┘
                    │
                    ▼
                PostgreSQL

Capability View

Another view:

Order Management Backend

├── product
│   ├── api
│   ├── application
│   ├── domain
│   └── persistence
│
├── inventory
│   ├── api
│   ├── application
│   ├── domain
│   └── persistence
│
├── order
│   ├── api
│   ├── application
│   ├── domain
│   └── persistence
│
├── payment
│   ├── application
│   ├── domain
│   └── integration
│
├── security
│
└── shared
    └── only genuinely cross-cutting concerns

This is the direction we will carry into implementation।


Do We Need a Separate Customer Module?

Current requirements do not justify a rich Customer domain capability।

We mostly use externally authenticated customer identity।

Therefore we do not create:

customer/
├── CustomerController
├── CustomerService
├── CustomerRepository
...

without a requirement।

Customer identity can initially live within security/customer-context concepts needed by Order workflows।

This follows our scope discipline।


Does Payment Need an API Package?

Payment is triggered through Order payment behaviour:

POST /orders/{id}/pay

or whatever endpoint we finalize later।

Therefore payment does not necessarily need its own public:

/payment

API resource।

Order API can expose the customer-facing action while payment integration remains its own internal capability।

Again:

Internal capability boundaries do not need to mirror public URL structure one-to-one.


API Resource Model vs Internal Architecture

Externally:

/orders
/products
/inventory

Internally:

order
payment
inventory
product
security

These do not have to match exactly։

Public API represents consumer-facing resources/actions।

Internal architecture represents responsibility boundaries।


Avoid Creating a Generic "Service Layer"

We should not have:

service/

containing every application class merely because architecture diagrams often say Service Layer।

Capability-oriented package + clear class responsibility gives better context।

A developer reading:

order/application/CreateOrder

understands both capability and responsibility।


Naming Application Components

Possible naming styles:

CreateOrderUseCase

or:

CreateOrderService

or:

OrderApplicationService

All can work।

We should prioritize clarity and consistency rather than pattern ideology।

For this course, we can keep names simple and behaviour-oriented։


Our Initial Preference

Where workflow is substantial:

CreateOrder
CancelOrder
PayOrder

clear use-case-oriented components are useful।

For simple Product operations, one cohesive:

ProductApplicationService

may be enough।

We do not require one class per action mechanically।


Package Boundaries Are Not Security Boundaries

Putting code in:

payment/

does not technically prevent:

order/

from importing arbitrary internal classes।

Developers still need discipline।

Later we could enforce architecture rules, but current project will rely on clear design + review。


Architecture Review Through Scenarios

Let's test the architecture with actual scenarios।


Scenario 1 — Browse Products

Request:

GET /products

Flow:

ProductController
    ↓
BrowseProducts
    ↓
Product persistence
    +
Inventory persistence
    ↓
response

Product/Inventory domain state helps determine orderability।

No Order or Payment dependency।

Good।


Scenario 2 — Admin Changes Product Price

Flow:

Product Admin API
    ↓
Product application workflow
    ↓
load Product
    ↓
change current price
    ↓
persist Product

Historical Order Items remain unchanged because Order owns historical price।

Good।


Scenario 3 — Create Order

Flow:

Order API
    ↓
CreateOrder
    ↓
Product
Inventory
Order
    ↓
repositories
    ↓
single local transaction

Good।


Scenario 4 — Cancel Order

Flow:

Order API
    ↓
CancelOrder
    ↓
verify ownership
    ↓
Order.cancel()
    +
Inventory.restore(...)
    ↓
single transaction

Good।


Scenario 5 — Pay Order

Flow:

Order API
    ↓
PayOrder
    ↓
Order payment eligibility
    ↓
PaymentGateway
    ↓
Provider Client
    ↓
External Provider

Then local result handling।

Good conceptually, while failure semantics remain later design work।


Scenario 6 — Customer Requests Another Customer's Order

Flow:

Authenticated customer
    ↓
GetOrder
    ↓
load order
    ↓
ownership check fails

Security infrastructure authenticates identity।

Application workflow protects resource ownership।

Good।


Scenario 7 — Two Customers Buy Last Item

Architecture recognizes:

CreateOrder transaction
+
Inventory persistence concurrency control

must protect inventory state।

Exact strategy pending।

Good।


Architecture Non-Goals

Our initial architecture intentionally does not include:

Microservices

Kafka

Redis

CQRS

Event Sourcing

Domain Event Bus

Command Bus

Service Mesh

Multiple databases

Separate inventory service

Separate order service

Generic payment plugin framework

None are required for current v1।


Why No Redis?

We have no proven caching problem।

Adding Redis would introduce:

cache invalidation

additional runtime dependency

consistency questions

Without need।

PostgreSQL will be our source of truth।

If later profiling shows a caching need, architecture can evolve।


Why No Kafka?

Current workflows require synchronous consistency, especially Order + Inventory।

No current requirement needs asynchronous event distribution।

Kafka would solve a problem we do not have yet।


Why No CQRS?

Current read and write models are not complex enough to justify separate architectures।

Normal application + PostgreSQL queries are sufficient।


Why No Event Sourcing?

We need historical Order data, but historical data does not automatically mean event sourcing।

Persisting current Order state plus historical item facts satisfies current requirement।

Event sourcing would introduce major complexity without justification।


Why No Generic "Clean Architecture" Framework?

We use architecture principles that resemble ports/adapters in useful places:

PaymentGateway
Repositories

but we will not force every dependency through multiple abstract layers।

Principles matter more than pattern labels।


Architecture and Future Courses

This architecture intentionally establishes a strong baseline।

Later Backend Engineering course may introduce requirements that pressure this design:

higher traffic

async processing

caching

event distribution

service decomposition

At that point learners can see:

Why would we evolve beyond this architecture?

That is much more useful than starting with complexity before experiencing the problem।


Architecture and the RFC

We now have enough design material for the next lessons।

Our RFC can capture:

Context

Goals

Non-Goals

Domain Model

Architecture

Major Workflows

Persistence Strategy

Transaction Requirements

Security Boundary

External Payment Integration

Risks

Alternatives

We will not need to invent architecture inside the RFC because we have already reasoned through it।


Decisions That May Deserve ADRs

Some current decisions are durable enough that ADRs may be useful।

Potential examples:

Use a modular monolith rather than microservices.
Use PostgreSQL as the application transactional database.
Preserve purchase-time price inside Order Items.
Keep Payment Provider protocol behind an application-facing boundary.

But we won't create ADRs for every detail।

The dedicated ADR lesson will decide what deserves one।


What Is Still Open?

Architecture is clearer, but some implementation-level questions remain:

Exact package/class names

Exact JPA model

Repository implementation style

Database schema

ID generation strategy

Money representation

Inventory concurrency strategy

Payment persistence model

API endpoint/error contract details

These are intentionally unresolved।

Architecture should not pretend to answer everything։


Architecture Decision Checklist

Before moving forward, let's test our initial architecture।

Requirement Fit

Does it support our v1 capabilities?

Yes।

Simplicity

Does it introduce distributed infrastructure?

No।

Ownership

Are Product, Inventory, Order, Payment boundaries clear?

Yes।

Transactions

Can local Order + Inventory workflows be atomic?

Yes, within one application/database।

Security

Can authentication and ownership remain separate concerns?

Yes।

External Integration

Is provider-specific payment logic isolated?

Yes।

Testability

Can domain, workflow, persistence, and API behaviour be tested at appropriate levels?

Yes।

Future Change

Can we modify one capability without automatically rewriting the whole application?

The structure is designed to support that।


Our Initial Architecture Decision

We can now summarize the proposal:

The Order Management Backend will be implemented as a single Spring Boot application organized around business capabilities. Each capability will keep HTTP, application workflow, domain, and persistence/integration responsibilities clearly separated where needed. PostgreSQL will store application-owned state. Local multi-entity business operations such as order creation and cancellation will use database transactions. Payment-provider communication will be isolated behind an application-facing integration boundary. Existing identity infrastructure will authenticate users, while application workflows will enforce resource ownership and business authorization.

This is our initial architecture।


Engineering Principle

The core principle from this lesson:

Architecture should make the correct place for behaviour easier to recognize.

Another:

Keep business capabilities logically separated without distributing them across the network before there is a reason to do so.

And:

Use the simplest architecture that protects current requirements, invariants, and system boundaries.


Summary

In this lesson, we established that:

  • The application will be a modular monolith.
  • It will remain one Spring Boot deployment unit.
  • Code will be organized primarily around business capabilities.
  • Product, Inventory, Order, Payment, and Security are the main capability boundaries.
  • Within capabilities, API, application, domain, and infrastructure responsibilities can be separated where useful.
  • Domain should remain focused on business behaviour.
  • Application workflows coordinate use cases and transactions.
  • Controllers handle HTTP concerns.
  • Persistence code handles PostgreSQL/JPA concerns.
  • Payment-provider protocol stays inside integration code.
  • Customer authentication comes from the external identity capability.
  • Application workflows enforce resource ownership.
  • Order creation and cancellation are local transactional operations.
  • Payment crosses an external boundary and needs separate consistency reasoning.
  • Product and Inventory remain separate concepts.
  • Order owns Order Items but not Product or Inventory.
  • Request/response DTOs are not domain objects.
  • Repository abstraction should remain pragmatic rather than ceremonial.
  • We will not add interfaces, mappers, layers, or adapters unless they solve a real responsibility problem.
  • We will not use microservices, Kafka, Redis, CQRS, Event Sourcing, or similar infrastructure in v1.
  • Several implementation details remain intentionally open for later lessons.

Next lesson:

Writing Our First RFC

There we will take the requirements, domain model, responsibility boundaries, architecture decisions, risks, non-goals, and major workflows we have established and turn them into a real technical proposal that another engineering team member could review before implementation begins.