Designing the System
Designing the Initial Architecture
আপনি একটি 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:
@Transactionalis 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.