Building the Spring Boot Application
Structuring a Backend Codebase
আপনি একটি free preview lesson দেখছেন।
আমরা এখন পর্যন্ত Spring Boot application bootstrap করেছি এবং কয়েকটি foundational concept শিখেছি:
Dependency Injection
Beans
Application Context
Configuration
Environment-Specific Configuration
এখন application grow করা শুরু করলে একটি practical question সামনে আসে:
নতুন code কোথায় রাখব?
একটি backend codebase ছোট থাকা অবস্থায় প্রায় যেকোনো structure কাজ করতে পারে।
কিন্তু system grow করলে poor structure দ্রুত problem হয়ে যায়।
আমাদের Order Management Backend eventually contain করবে:
Product
Inventory
Order
Payment
Security
প্রতিটি capability-এর মধ্যে আবার থাকবে:
incoming request handling
application workflow
domain behaviour
persistence
external integration
এই lesson-এর goal:
একটি code structure establish করা যা business capability এবং responsibility দুটোই পরিষ্কার রাখে।
Structure Should Reflect the System
একটি codebase খুলে একজন engineer ideally বুঝতে পারবে:
এই application কী কী business capability নিয়ে কাজ করে?
একটি request কোথা দিয়ে ঢোকে?
Business operation কোথায় coordinate হয়?
Domain rules কোথায় থাকে?
Database access কোথায় থাকে?
External systems কোথায় integrate হয়?
যদি structure এই প্রশ্নগুলোর উত্তর না দেয়, তাহলে engineers-কে class-by-class search করতে হয়।
Good structure reduces that cognitive cost।
Our Main Application Flow
আমাদের internal convention:
Handler
↓
UseCase
↓
Repository
যদি external dependency থাকে:
Handler
↓
UseCase
├── Repository
└── External Service
Domain objects are used by the UseCase:
Handler
↓
UseCase
↓
Domain
↓
Repository
More realistically:
Handler
↓
UseCase
├── Domain
├── Repository
└── External Service
এটাই package structure-এর foundation।
Capability First
Top-level package structure business capabilities follow করবে।
Eventually:
io.liveklass.ordermanagement
├── product
├── inventory
├── order
├── payment
└── security
এই structure immediately বলে দেয় application-এর main responsibilities কী।
Compare with:
controller
service
repository
entity
dto
utils
এই structure technical categories দেখায়, business system নয়।
Why Not Global Layer-First Packages?
Suppose:
controller/
├── ProductController
├── InventoryController
├── OrderController
└── PaymentController
service/
├── ProductService
├── InventoryService
├── OrderService
└── PaymentService
repository/
├── ProductRepository
├── InventoryRepository
└── OrderRepository
System grow করলে Product-related code multiple folders-এ ছড়িয়ে যায়।
To understand Product capability, engineer must jump through:
controller
service
repository
entity
dto
We prefer keeping related code close।
Capability-Oriented Structure
Instead:
product/
inventory/
order/
payment/
security/
Then each capability can organize its own responsibilities।
For example:
order/
├── handler/
├── usecase/
├── domain/
└── repository/
This combines:
business capability
+
responsibility separation
Our Package Convention
For a capability with enough complexity:
<capability>/
├── handler/
├── usecase/
├── domain/
└── repository/
If external integration belongs to that capability:
<capability>/
└── service/
But remember our terminology:
serviceis reserved for external/third-party capabilities.
So internal application workflow does not go into service/.
Example: Order Capability
Conceptually:
order/
├── handler/
│ ├── CreateOrderHandler.java
│ ├── CancelOrderHandler.java
│ └── GetOrderHistoryHandler.java
│
├── usecase/
│ ├── CreateOrderUseCase.java
│ ├── CancelOrderUseCase.java
│ └── GetOrderHistoryUseCase.java
│
├── domain/
│ ├── Order.java
│ ├── OrderItem.java
│ └── OrderStatus.java
│
└── repository/
└── OrderRepository.java
Exact classes will appear as tickets are implemented।
We do not create all of them now।
This is the structural direction।
What Belongs in handler/?
Handler is an application entry point।
For HTTP-based behaviour, Handler receives request data and calls the relevant UseCase।
Conceptually:
public class CreateOrderHandler {
private final CreateOrderUseCase useCase;
public CreateOrderHandler(
CreateOrderUseCase useCase
) {
this.useCase = useCase;
}
public OrderResponse handle(
CreateOrderRequest request
) {
// map input
// call use case
// map output
return null;
}
}
Handler should deal with concerns such as:
incoming request
transport-level validation
request mapping
authenticated request context
response mapping
It should not own the core business workflow।
Handler Is Not the Business Logic Layer
Bad:
public class CreateOrderHandler {
public OrderResponse handle(
CreateOrderRequest request
) {
// query products
// check inventory
// calculate total
// save order
// update inventory
}
}
Now Handler owns too much।
Better:
Handler
↓
CreateOrderUseCase
The UseCase coordinates the operation।
What Belongs in usecase/?
UseCase represents a meaningful application operation।
Examples:
CreateOrderUseCase
CancelOrderUseCase
PayOrderUseCase
CreateProductUseCase
AdjustInventoryUseCase
A UseCase answers:
What must the application do to complete this business operation?
Example: CreateOrderUseCase
Conceptually:
public class CreateOrderUseCase {
private final ProductRepository productRepository;
private final InventoryRepository inventoryRepository;
private final OrderRepository orderRepository;
public CreateOrderUseCase(
ProductRepository productRepository,
InventoryRepository inventoryRepository,
OrderRepository orderRepository
) {
this.productRepository = productRepository;
this.inventoryRepository =
inventoryRepository;
this.orderRepository = orderRepository;
}
public Order execute(
CustomerId customerId,
CreateOrderCommand command
) {
// coordinate the workflow
return null;
}
}
UseCase may:
load state
coordinate domain objects
enforce ownership/application rules
call repositories
call an external Service
define transaction boundary
where appropriate।
UseCase Is Not a Generic Service
Avoid:
OrderService
containing:
createOrder()
cancelOrder()
payOrder()
getHistory()
adminUpdate()
This grows into a broad procedural class।
Operation-oriented UseCases make responsibilities clearer।
When Multiple Operations Can Share One UseCase Class
We do not need one class per method mechanically।
If several tiny operations are highly cohesive, grouping can sometimes be reasonable।
But the default should be:
A substantial business operation deserves an explicit, understandable application boundary.
For our important workflows:
CreateOrderUseCase
CancelOrderUseCase
PayOrderUseCase
are clear and appropriate।
What Belongs in domain/?
Domain contains business concepts and rules।
For Order:
Order
OrderItem
OrderStatus
Domain answers questions like:
Can this Order be cancelled?
Can this Order be paid?
What is the total?
Is this Order state valid?
It should not know:
HTTP
Spring MVC
database URLs
JPA repositories
Payment Provider JSON
Example: Order Domain Behaviour
Conceptually:
public class Order {
private OrderStatus status;
public void cancel() {
if (status != OrderStatus.UNPAID) {
throw new IllegalStateException(
"Order cannot be cancelled"
);
}
status = OrderStatus.CANCELLED;
}
}
The exact exception model will evolve later।
Important point:
Order protects Order lifecycle
rather than UseCase performing arbitrary setter mutation।
Domain Objects Are Not Spring Components by Default
We generally do not write:
@Component
public class Order {
}
Order is business state।
UseCases create or load Orders as needed।
Spring manages reusable application components around the domain।
What Belongs in repository/?
Repository represents persistence access for a capability।
For example:
public interface OrderRepository {
Order save(Order order);
Optional<Order> findById(OrderId orderId);
}
Exact methods should follow real UseCase requirements।
Avoid designing huge generic repository APIs upfront।
Repository Should Support UseCases
Repository design should be driven by application needs।
For example:
CreateOrderUseCase
may need:
save order
Order history may need:
find orders by customer
Cancellation may need:
load order by ID
So repository evolves as workflows require operations।
Repository Is Not a Generic Database Wrapper
Avoid:
public interface GenericRepository<T> {
T save(T value);
T find(Long id);
void update(T value);
void delete(Long id);
}
just to create abstraction।
Different capabilities have different persistence needs।
For example Inventory may eventually need a concurrency-safe operation that Product does not।
Repository should express meaningful capability-specific persistence operations।
Repository vs Spring Data Repository
Later we may use Spring Data JPA।
There are multiple possible structures:
UseCase
↓
OrderRepository
↓
Spring Data implementation
or, for simple cases, a Spring Data repository may itself satisfy the required persistence boundary।
We have intentionally not forced one approach for every capability।
Avoid wrapper layers that add no value।
Persistence Implementation Placement
As persistence complexity grows, repository/ may need internal separation।
For example:
order/
└── repository/
├── OrderRepository.java
├── JpaOrderRepository.java
└── SpringDataOrderRepository.java
But don't introduce three layers simply because that structure appears in architecture tutorials।
Use the minimum structure required by real persistence concerns।
What Belongs in service/?
In our application terminology:
service/
is for external/third-party capabilities।
For example Payment:
payment/
└── service/
├── PaymentService.java
└── ProviderPaymentService.java
Where:
PaymentService
is the application-facing external capability boundary।
And:
ProviderPaymentService
handles the specific provider integration।
External Service Responsibility
ProviderPaymentService may know:
provider URL
authentication
HTTP protocol
provider request schema
provider response schema
timeouts
provider-specific errors
PayOrderUseCase should not know these details।
Flow:
PayOrderUseCase
↓
PaymentService
↓
ProviderPaymentService
↓
Payment Provider
Why Payment Is Its Own Capability
Payment represents an external integration with its own responsibility।
A future shape might be:
payment/
├── service/
│ ├── PaymentService.java
│ └── ProviderPaymentService.java
│
└── configuration/
└── PaymentProperties.java
We do not yet create a payment/domain/ package because no dedicated Payment domain model has been established।
This follows our design review decision।
Configuration Placement
Capability-specific configuration should stay near the capability।
For Payment:
payment/
├── configuration/
│ ├── PaymentConfiguration.java
│ └── PaymentProperties.java
└── service/
Better than:
config/
├── PaymentConfig
├── DatabaseConfig
├── SecurityConfig
├── SomethingElseConfig
where unrelated concerns accumulate।
Application-Wide Configuration
Some configuration may genuinely belong at application level।
For example:
OrderManagementApplication.java
or possibly truly cross-cutting framework configuration।
But use a global configuration package only when responsibility is genuinely global।
Product Capability
As Product implementation grows, it may become:
product/
├── handler/
│ ├── CreateProductHandler.java
│ └── BrowseProductsHandler.java
│
├── usecase/
│ ├── CreateProductUseCase.java
│ └── BrowseProductsUseCase.java
│
├── domain/
│ └── Product.java
│
└── repository/
└── ProductRepository.java
Again, this is a direction, not a demand to scaffold every file today।
Inventory Capability
Similarly:
inventory/
├── handler/
│ └── AdjustInventoryHandler.java
│
├── usecase/
│ └── AdjustInventoryUseCase.java
│
├── domain/
│ └── Inventory.java
│
└── repository/
└── InventoryRepository.java
Order creation can depend on InventoryRepository without turning Inventory into an external Service।
Everything remains inside the monolith।
Security Capability
Security is slightly different।
Authentication comes from an existing identity mechanism।
Our backend needs:
authenticated identity
roles/authorities
request security configuration
So later structure could be something like:
security/
├── configuration/
├── authentication/
└── context/
Exact structure will be driven by Spring Security implementation।
We should not force handler/usecase/repository onto capabilities where those responsibilities do not naturally exist।
Important: Structure Is Not a Template to Copy Blindly
Not every capability needs:
handler/
usecase/
domain/
repository/
service/
configuration/
all at once।
That would recreate the same over-engineering problem in a different form।
For each folder ask:
Is there a real responsibility here now?
If no:
do not create it yet
Example: Payment Today
Given our current design, Payment might eventually need:
payment/
├── service/
└── configuration/
It may not need:
payment/repository/
until payment persistence requirements actually exist।
It may not need:
payment/domain/
until a real Payment domain model exists।
This is intentional restraint।
Flat First, Split When Useful
Suppose Product initially contains only:
Product.java
CreateProductUseCase.java
ProductRepository.java
You could initially keep them under:
product/
if the capability is tiny।
As it grows:
product/
├── handler/
├── usecase/
├── domain/
└── repository/
becomes worthwhile।
Structure should solve real navigation complexity।
Consistency Still Matters
Flexibility does not mean every capability should be organized randomly।
Our shared conventions are:
Handler
→ incoming application boundary
UseCase
→ application workflow
Repository
→ persistence
Service
→ third-party/external capability
Domain
→ business state and behaviour
The exact number of folders can vary while these meanings remain stable।
Request and Response Models
Where should HTTP request/response models live?
Near the Handler/API boundary।
For example:
order/
└── handler/
├── CreateOrderHandler.java
├── CreateOrderRequest.java
└── OrderResponse.java
or later a substructure if needed।
They should not live in:
order/domain/
because API transport representation and domain representation are different responsibilities।
Command Objects
A Handler may map transport input into an application command।
Example:
CreateOrderRequest
↓
CreateOrderCommand
↓
CreateOrderUseCase
A CreateOrderCommand belongs near application/UseCase concerns, not HTTP transport।
Possible structure:
order/usecase/
├── CreateOrderUseCase.java
└── CreateOrderCommand.java
This keeps UseCase input independent of Spring MVC request classes।
Do We Always Need Commands?
No।
If a UseCase takes two simple parameters:
execute(
CustomerId customerId,
ProductId productId
)
creating a command class may add no value।
Use a command when input becomes meaningfully structured।
Don't create DTOs merely because architecture diagrams contain them।
Handler DTOs Are Not Domain Objects
Example request:
public record CreateOrderRequest(
List<ItemRequest> items
) {
}
This represents incoming transport data।
Domain might eventually use:
Order
OrderItem
They should not be the same object merely to avoid mapping।
Why?
Because transport input is untrusted।
For example client must not supply authoritative:
price
order total
customer owner
Those come from server-side state/context।
Transport Model vs Application Model
Useful flow:
HTTP JSON
↓
Request Model
↓
Handler
↓
Command / UseCase Input
↓
UseCase
↓
Domain
We do not need all these types for every trivial endpoint।
But responsibility boundary should remain clear।
Don't Create a Global dto/ Package
Avoid:
dto/
├── ProductDto
├── OrderDto
├── InventoryDto
└── PaymentDto
because this separates transport models from the capability that owns them।
Keep DTOs close to the Handler/API responsibility that uses them।
Don't Create a Global entity/ Package
We already use "entity" as a domain concept।
A global:
entity/
often becomes a mixture of JPA and domain terminology।
Prefer:
order/domain/Order
and keep persistence-specific representation in repository/persistence code if separate representations become necessary।
Domain and Persistence Representation
Later we must decide whether:
Order
is also a JPA-mapped class or whether persistence uses a separate representation।
That decision is intentionally deferred।
Our package structure should not force either choice prematurely।
Avoid Mapping Layers by Default
Some architectures automatically create:
Order
OrderEntity
OrderDto
OrderMapper
OrderEntityMapper
OrderResponseMapper
before there is any complexity।
This can make simple code difficult to navigate।
We will separate representations when boundaries genuinely require it, but avoid ceremonial mapping layers।
Keep Mapping Close to the Boundary
If HTTP mapping is simple, Handler may map directly।
If mapping becomes large/repeated, a dedicated mapper near the Handler may be justified।
Same for persistence mapping।
Don't create one global:
mapper/
containing unrelated mappings।
Cross-Capability Workflows
CreateOrderUseCase requires Product, Inventory, and Order persistence।
Where should it live?
order/usecase/
because the operation's business outcome is creating an Order।
It may depend on:
ProductRepository
InventoryRepository
OrderRepository
This is acceptable inside the modular monolith।
Capability Boundary Does Not Mean Isolation
Our packages are not microservices।
This:
order/usecase/CreateOrderUseCase
can call application-appropriate repository boundaries from Product and Inventory।
We do not need:
HTTP call to Product module
HTTP call to Inventory module
inside one application।
Avoid Internal Network Thinking
Bad modular-monolith design:
CreateOrderUseCase
↓ HTTP
Product module
↓ HTTP
Inventory module
This introduces network-style complexity inside one deployable application।
Use normal Java dependencies।
Mutation Ownership Still Matters
Although capabilities can collaborate, ownership should remain clear।
Product writes belong to Product workflows।
Inventory changes belong to Inventory-related operations or workflows such as Order creation/cancellation that explicitly coordinate inventory。
Order owns Order lifecycle।
Avoid arbitrary cross-capability mutation from unrelated code।
Cross-Capability Domain Calls
Avoid:
order.getInventory().decrease(...)
if Inventory has independent lifecycle/ownership।
Likewise avoid:
product.createOrder(...)
just because Product is involved in ordering।
Application UseCase coordinates the collaboration।
Example: Cancellation
Structure:
order/
└── usecase/
└── CancelOrderUseCase
Dependencies:
OrderRepository
InventoryRepository
Flow:
CancelOrderHandler
↓
CancelOrderUseCase
↓
load Order
↓
verify ownership
↓
Order.cancel()
↓
restore Inventory
↓
save changes
The cross-capability coordination belongs in the UseCase।
Avoid UseCase-to-UseCase Chains Without Need
Possible bad flow:
CancelOrderUseCase
↓
CancelOrderStatusUseCase
↓
RestoreInventoryUseCase
↓
SaveOrderUseCase
Now one business operation is fragmented into many application components।
UseCases are not tiny method wrappers।
One UseCase should coordinate the meaningful operation।
Reuse Domain and Repository Behaviour Instead
If multiple UseCases need:
load Order
they can both use OrderRepository।
If multiple workflows need:
Order cancellation rule
that rule belongs in Order.cancel()।
We do not need to call another UseCase just for reuse।
Handler-to-Handler Calls Are Also Wrong
Avoid:
CreateOrderHandler
↓
InventoryHandler
Handlers represent incoming boundaries।
Internal workflows should call application/domain/repository collaborators directly, not route through another transport Handler।
Package Dependencies
A healthy conceptual dependency direction:
handler
↓
usecase
↓
domain
And:
usecase
↓
repository
External:
usecase
↓
service
Repository/service implementation may depend on framework/infrastructure libraries।
Domain Should Not Depend Upward
Avoid:
domain
↓
handler
or:
domain
↓
usecase
or:
domain
↓
Spring HTTP
Domain should remain focused on business concepts।
Repository Should Not Own UseCase Logic
Bad:
public Order createOrder(...) {
// validate products
// check inventory
// construct Order
// save everything
}
inside OrderRepository।
Repository should own persistence operations, not the entire business workflow।
External Service Should Not Mutate Domain State Directly
Bad:
ProviderPaymentService
↓
loads Order
↓
marks Order paid
↓
saves Order
Now external integration owns Order workflow।
Better:
PayOrderUseCase
↓
PaymentService
↓
returns result
Then UseCase decides how application/domain state changes।
Keep External Protocol Types at the Boundary
Provider may return:
{
"provider_status": "CAPTURED",
"transaction_ref": "abc123"
}
Don't pass provider-specific response object throughout the application।
Map it into an application-facing result:
PaymentResult
near the external Service boundary।
This protects the rest of the codebase from provider protocol changes।
Shared Code
Eventually multiple capabilities may genuinely share something।
But avoid creating:
shared/
at bootstrap merely because we expect reuse।
Once a real shared concept exists, ask:
Is this truly application-wide?
Does it have one coherent meaning?
Would capability ownership be misleading?
Only then move it into shared space।
What Might Be Legitimately Shared?
Potential examples later:
common API error representation
pagination primitives
technical correlation ID handling
But even these should be introduced only when they become real requirements।
Avoid utils/
A generic:
utils/
often hides missing ownership।
Example:
OrderUtils.calculateTotal(...)
likely belongs in Order domain।
InventoryUtils.validateQuantity(...)
likely belongs in Inventory।
Ask:
Which responsibility naturally owns this behaviour?
Avoid helpers/
Same issue:
ProductHelper
OrderHelper
PaymentHelper
These names rarely explain responsibility।
Prefer explicit domain/application/integration types।
Avoid Generic manager/
Class:
OrderManager
does not tell us if it:
handles HTTP
coordinates workflows
persists data
updates domain state
Our terminology is intentionally more specific:
Handler
UseCase
Repository
Service
Avoid Internal service/
Because our project reserves Service for external systems, this structure would be misleading:
order/
└── service/
└── OrderService.java
Internal workflow belongs under:
order/usecase/
This vocabulary consistency will matter as the codebase grows।
Naming Handlers
Prefer action-oriented names:
CreateOrderHandler
CancelOrderHandler
GetOrderHistoryHandler
instead of overly generic:
OrderHandler
when the Handler would otherwise contain many unrelated operations।
However, exact HTTP implementation may group endpoints later where practical।
Responsibility clarity matters more than rigid one-endpoint-one-class rules।
Naming UseCases
Prefer verbs/outcomes:
CreateOrderUseCase
CancelOrderUseCase
PayOrderUseCase
BrowseProductsUseCase
AdjustInventoryUseCase
These immediately communicate application behaviour।
Naming Repositories
Repositories typically represent the aggregate/capability state they persist:
OrderRepository
ProductRepository
InventoryRepository
Avoid:
DataRepository
GenericRepository
RepositoryManager
because they hide ownership।
Naming External Services
Use external capability meaning:
PaymentService
IdentityService
NotificationService
Implementation can identify provider if useful:
ProviderPaymentService
or a real provider-specific name later।
Keep Provider Names Out of UseCases
Prefer:
private final PaymentService paymentService;
not:
private final SomeProviderPaymentService paymentService;
if the UseCase should remain provider-independent।
Infrastructure knows provider identity।
Application workflow knows capability।
Package-Private Visibility
Not every class needs to be public।
Java package-private visibility can help keep internal details local।
For example, a mapper/helper used only inside one package may not need public visibility।
Smaller public surface reduces accidental coupling।
Don't Make Everything Public by Habit
If another package does not need a class, ask whether it really needs:
public
This is especially useful for implementation details।
However, package design should not become artificially complex just to exploit package-private visibility।
Circular Dependencies
Good package structure should help us notice cycles।
Example problem:
order
↓
inventory
↓
order
This may indicate the two capabilities are calling each other's application logic incorrectly।
UseCase coordination can often remove the cycle।
Example of a Bad Cycle
CancelOrderUseCase
↓
AdjustInventoryUseCase
↓
OrderRepository
Then Inventory workflow becomes dependent on Order details।
Better:
CancelOrderUseCase
├── OrderRepository
└── InventoryRepository
One workflow owns cancellation consistency।
Circular Spring Dependencies Are Usually Design Feedback
Spring may report a dependency cycle during startup।
Don't immediately try to solve it with:
lazy injection
setter injection
ApplicationContext lookup
First ask:
Why are these components mutually dependent?
Often architecture should be corrected instead।
Package Structure and Transactions
Transaction boundary generally belongs to the UseCase coordinating a local atomic operation।
For example:
CreateOrderUseCase
coordinates:
Inventory update
Order persistence
So later transaction management belongs around that operation।
Repository classes should not independently create conflicting transaction semantics for the same workflow without a reason।
Package Structure and Security
Handler may obtain authenticated request context।
UseCase may receive:
CustomerId
or another application-friendly identity representation।
Domain should not parse:
Authorization header
JWT
Spring SecurityContext
Those belong at the security/application boundary।
Example
Conceptually:
HTTP Request
↓
Security
↓
CreateOrderHandler
↓
authenticated CustomerId
↓
CreateOrderUseCase
Then:
Order
only knows its owner identity, not authentication mechanics।
Package Structure and Configuration
Configuration belongs near infrastructure that uses it।
For Payment:
payment/
├── configuration/
└── service/
For database infrastructure, Spring's standard configuration may live at application/persistence level as needed।
Handlers and UseCases should not contain:
property names
environment lookups
credentials
What About Exceptions?
We will eventually need errors such as:
OrderNotFound
InsufficientInventory
OrderNotCancellable
Don't immediately create:
exception/
at root।
Keep errors near the capability/responsibility that owns them।
For example:
order/domain/
or:
order/usecase/
depending on what the failure represents।
Later API error mapping can translate them at Handler/API boundary।
Domain Error vs Application Error
Example:
Order cannot transition from PAID to CANCELLED
is closely related to domain behaviour।
Example:
Order does not belong to current customer
may be an application/authorization failure।
Exact error model will evolve later।
Structure should follow meaning rather than one global exception folder।
What About Constants?
Avoid:
constants/
containing random values from every capability।
A constant usually belongs near the concept that owns it।
If:
OrderStatus.UNPAID
is domain concept, keep it in domain model।
Configuration values that vary by environment are not Java constants at all।
What About Enums?
Don't create:
enums/
at root।
OrderStatus belongs with Order domain:
order/domain/OrderStatus.java
The type's meaning is more important than its Java language category।
Organize by Meaning, Not Java Syntax
Avoid top-level folders like:
interfaces/
enums/
records/
exceptions/
These tell us what language feature a file uses, not what part of the system it belongs to।
Prefer business ownership first।
Don't Create a Package Per Design Pattern
Avoid:
factory/
strategy/
adapter/
builder/
at root just because those patterns may appear।
A Payment adapter belongs to Payment।
An Order factory, if one is genuinely needed, belongs near Order domain/application।
Patterns are implementation techniques, not business capabilities।
Repository Root Structure
Eventually the repository may look like:
order-management/
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── io/liveklass/ordermanagement/
│ │ └── resources/
│ └── test/
│
├── docs/
│ ├── rfcs/
│ ├── adr/
│ └── incidents/
│
├── build.gradle
├── settings.gradle
└── README.md
Later Docker/Compose/CI files arrive when their tickets require them।
Java Source Direction
Inside:
io/liveklass/ordermanagement/
eventually:
OrderManagementApplication.java
product/
inventory/
order/
payment/
security/
No need to create every capability folder before implementation begins।
Example Mature Structure
A reasonable later-stage structure could resemble:
io/liveklass/ordermanagement/
├── OrderManagementApplication.java
│
├── product/
│ ├── handler/
│ ├── usecase/
│ ├── domain/
│ └── repository/
│
├── inventory/
│ ├── handler/
│ ├── usecase/
│ ├── domain/
│ └── repository/
│
├── order/
│ ├── handler/
│ ├── usecase/
│ ├── domain/
│ └── repository/
│
├── payment/
│ ├── configuration/
│ └── service/
│
└── security/
├── configuration/
└── authentication/
This is illustrative, not a scaffold to generate today।
Structure Should Grow With Tickets
Example progression:
BACKEND-103:
product/domain/
becomes real।
BACKEND-104:
product/repository/
becomes real।
Product API work:
product/handler/
becomes real।
Order workflows:
order/usecase/
becomes real।
Payment integration:
payment/service/
payment/configuration/
becomes real।
The repository structure tells the same story as the backlog।
Don't Build Future Architecture as Empty Files
Bad first commit:
product/handler/.keep
product/usecase/.keep
product/domain/.keep
product/repository/.keep
inventory/...
order/...
payment/...
This adds noise without functionality।
Architecture already exists in RFC/ADR।
Code structure should emerge with implementation।
Tests Should Mirror Capability Structure
If main source:
order/domain/Order.java
then test:
src/test/java/.../order/domain/OrderTest.java
If main:
order/usecase/CreateOrderUseCase.java
then:
order/usecase/CreateOrderUseCaseTest.java
This makes tests predictable to find।
Handler Tests
Handler tests later may focus on:
request mapping
transport validation
response mapping
UseCase behaviour should not be retested entirely through every Handler test।
Each layer/boundary should protect the behaviour it owns।
UseCase Tests
CreateOrderUseCaseTest may test coordination such as:
invalid Product fails
insufficient Inventory fails
successful Order persists
Inventory is decreased
whole workflow fails appropriately
using controlled repository dependencies।
Domain Tests
OrderTest focuses on:
Order lifecycle
Order total
invalid transitions
No Spring required।
Repository Tests
Persistence tests focus on:
mapping
queries
database constraints
transaction/concurrency behaviour
with PostgreSQL/Testcontainers later।
Code structure makes test responsibility clearer too।
Refactoring Package Structure
Package structure is not immutable।
If a capability grows and current layout becomes confusing, refactor it।
For example:
Before:
product/
├── Product.java
├── ProductRepository.java
├── CreateProductUseCase.java
└── CreateProductHandler.java
Later:
product/
├── handler/
├── usecase/
├── domain/
└── repository/
That's healthy evolution।
Don't Reorganize Without a Problem
Constantly moving files because a new architecture article recommends something else creates:
merge conflicts
noisy history
navigation churn
Structure should change because current structure no longer communicates responsibilities well।
Avoid Architecture Fashion
We are not creating:
hexagonal/
clean/
ports/
adapters/
application-core/
infrastructure/
just because these terms are popular।
Some of their underlying principles are useful:
clear boundaries
dependency direction
external isolation
We are applying those ideas using terminology that remains understandable for this application।
Our Structure Is Pragmatic
We want enough separation to protect:
domain rules
application workflows
persistence
external integrations
without requiring dozens of files for a simple feature।
The target is:
clear, boring, navigable code.
Not architecture diagram sophistication।
A Practical Placement Exercise
Suppose we create:
CreateOrderRequest
Question:
Where?
Answer:
order/handler/
because it represents incoming transport data।
Suppose:
CreateOrderUseCase
Answer:
order/usecase/
Suppose:
OrderStatus
Answer:
order/domain/
Suppose:
OrderRepository
Answer:
order/repository/
Suppose:
ProviderPaymentService
Answer:
payment/service/
Suppose:
PaymentProperties
Answer:
payment/configuration/
This placement follows responsibility consistently।
Another Placement Exercise
Suppose:
OrderTotalCalculator
Before creating a class, ask:
Why does it exist?
If total is simply part of Order behaviour:
don't create separate component
Put behaviour on Order।
Structure should not encourage unnecessary classes।
Another Example: Current User
Suppose Spring Security integration provides:
AuthenticatedUser
This likely belongs under:
security/
not:
order/domain/
Even though Order UseCases consume customer identity।
Ownership remains Security/application context।
Another Example: Insufficient Inventory Error
This could belong near:
inventory/domain/
if it represents Inventory's inability to satisfy a quantity।
Or near Order UseCase if it represents an application operation result।
We decide based on semantics, not on a universal exceptions/ rule।
Architectural Boundaries Should Be Visible in Imports
When reviewing:
CreateOrderUseCase
expected imports may include:
ProductRepository
InventoryRepository
OrderRepository
Order
OrderItem
Unexpected imports:
HttpServletRequest
JpaRepository
ProviderPaymentResponse
ApplicationContext
would deserve questions।
Imports can reveal boundary violations quickly।
Handler Imports
A Handler may legitimately know:
request/response models
UseCase
authenticated request context
It should generally not know:
PostgresOrderRepository
EntityManager
provider client
Domain Imports
Domain should mostly depend on:
Java language/library types
other domain concepts
Framework dependencies should remain minimal。
This keeps business behaviour understandable and testable।
Repository Imports
Repository implementation may legitimately depend on:
JPA
Spring Data
database-related types
because that is its responsibility।
We do not pretend all framework dependencies are bad।
They should simply live at the correct boundary।
External Service Imports
ProviderPaymentService may use:
HTTP client
JSON mapping
provider-specific models
configuration
Again, appropriate because integration is its responsibility।
Architecture Is Dependency Discipline
Folders alone do not protect boundaries।
An engineer can still import anything public।
Therefore maintain structure through:
clear conventions
code review
focused tests
RFC/ADR context
Potentially architecture tests later if complexity justifies them, but not now।
Code Review Questions
When reviewing a new class, ask:
Which capability owns this?
Is it a Handler, UseCase, domain concept,
Repository, or external Service?
Is it in the correct package?
Does it depend on the right layer?
Is business logic leaking into Handler?
Is persistence logic leaking into UseCase?
Is provider logic leaking into domain?
Is this class needed at all?
These questions keep the structure healthy।
Structure Smell: Giant UseCase Package
If:
order/usecase/
eventually contains dozens of unrelated operations, that may indicate Order capability itself needs internal organization।
But don't solve that today।
Respond to actual complexity when it appears।
Structure Smell: Shared Becomes Huge
If:
shared/
becomes one of the largest packages, ownership is probably unclear।
Shared should remain exceptional, not the default home for reusable code।
Structure Smell: Everything Ends With Util
Likely missing domain/application ownership।
Structure Smell: Everything Is a Spring Bean
Likely business/domain logic has been turned into framework-managed procedural components।
Structure Smell: Repository Performs Workflows
Persistence boundary has absorbed application logic।
Structure Smell: UseCase Knows HTTP
Transport boundary is leaking inward।
Structure Smell: Domain Knows Provider JSON
External integration has leaked into business model।
Structure Smell: Service Means Three Different Things
In many codebases Service may mean:
application workflow
domain helper
external dependency
Our project intentionally avoids that ambiguity।
Here:
UseCase
→ internal application operation
Service
→ external/third-party capability
This naming rule should remain consistent।
Current BACKEND-101 Scope
Even after learning all of this, we should not generate all packages now।
Current project can still remain:
io/liveklass/ordermanagement/
└── OrderManagementApplication.java
until actual capability work begins।
This lesson establishes the structure we will follow later।
Why Teach Structure Before Features?
Because once Product implementation begins, we should already know:
where Product belongs
where its UseCases belong
where persistence belongs
how Handler connects inward
That avoids reorganizing code immediately after writing it।
Structure Should Support the Next Ticket
The next meaningful capability work eventually includes Product and persistence。
When those tickets arrive, code placement will be deliberate rather than improvised।
This is enough architecture preparation।
Engineering Principle
The core principle:
Organize code around business capability first, then separate responsibilities inside that capability where the complexity justifies it.
Another:
Handler receives the application request, UseCase coordinates the operation, Repository owns persistence, domain objects protect business state, and Service represents an external capability.
And:
Create structure for responsibilities that exist today—not empty abstractions for features that may exist tomorrow.
Summary
In this lesson, we learned that:
- Code structure should make application responsibilities easy to discover.
- Our top-level organization is capability-oriented.
- Main capabilities will include Product, Inventory, Order, Payment, and Security.
- Our internal application flow is
Handler → UseCase → Repository. - Domain objects represent business state and behaviour.
- External/third-party integrations use the
Serviceterminology. - Internal application workflows should not be named generic
Service. - Handler owns incoming transport/application-boundary concerns.
- UseCase coordinates one meaningful application operation.
- Repository owns persistence access.
- External Service owns third-party protocol/integration behaviour.
- Capability-first organization keeps related code close together.
- Not every capability needs every package.
- Empty package hierarchies should not be scaffolded before real implementation exists.
- Request/response models belong near the Handler boundary.
- UseCase input may use command objects when the input is complex enough to justify them.
- Transport models and domain objects should not be treated as the same thing automatically.
- Global
dto,entity,utils,helpers,manager, and genericservicepackages should be avoided. - Repository abstractions should evolve from actual persistence needs.
- UseCases may coordinate multiple capability repositories inside the modular monolith.
- Capability boundaries do not imply internal HTTP calls or microservices.
- Cross-capability business workflows should be coordinated by the appropriate UseCase.
- Domain rules should remain in domain objects where they naturally belong.
- External Service implementations should not mutate Order state directly.
- Provider-specific protocol types should remain at the integration boundary.
- Tests should generally mirror the capability and responsibility they protect.
- Package structure may evolve as real complexity grows.
- Framework or architecture fashion should not determine unnecessary layers.
- Folder structure alone does not enforce architecture; dependency discipline and code review still matter.
- The current bootstrap project does not yet need all these folders—the structure should emerge with implementation.
Next lesson:
Running the Application Locally
There we will complete the practical side of BACKEND-101 by establishing a repeatable local workflow: building from a clean checkout, running through Gradle and the packaged JAR, understanding startup logs and shutdown behaviour, and verifying that the application does not depend on IDE-specific state.