Persistence with PostgreSQL
Why Our Application Needs Persistence
আপনি একটি free preview lesson দেখছেন।
এখন পর্যন্ত আমাদের application-এর বেশিরভাগ design ছিল memory-এর মধ্যে থাকা objects এবং HTTP contracts নিয়ে।
আমরা model করেছি:
Product
Inventory
Order
OrderItem
এবং operations:
create Product
browse Products
set Inventory
create Order
cancel Order
pay Order
কিন্তু একটি fundamental problem এখনো solve হয়নি।
ধরুন application চলছে এবং memory-তে আছে:
Product P-100
Inventory = 12
Order O-1001
তারপর application restart হলো।
যদি state শুধু Java memory-তে থাকে:
সব data হারিয়ে যাবে
এটি production backend-এর জন্য acceptable নয়।
আমাদের এমন একটি persistence mechanism দরকার যা application process-এর lifetime-এর বাইরে state preserve করবে।
এই course-এ সেই responsibility নেবে:
PostgreSQL
এই lesson-এর goal:
কোন application state persist করতে হবে, কেন PostgreSQL দরকার, database-এর responsibility কোথায় শুরু ও শেষ হয়, এবং persistence-কে domain design-এর driver না বানিয়ে কীভাবে system architecture-এর অংশ হিসেবে ভাবতে হয়।
Memory Is Temporary
Java object:
Product product = new Product(...);
application process-এর memory-তে থাকে।
Application বন্ধ হলে:
process ends
↓
memory released
↓
object disappears
Production system-এ আমরা চাই না:
deployment
বা:
server restart
হওয়ার কারণে সব Order হারিয়ে যাক।
Persistent storage-এর মূল উদ্দেশ্য:
important state survives process lifetime
What State Must Survive?
আমাদের system-এর জন্য persistent state identify করা যাক।
Product
আমাদের preserve করতে হবে:
ProductId
name
current price
active state
কারণ application restart হলেও Product catalog হারানো যাবে না।
Inventory
Inventory state:
ProductId
available quantity
persist করতে হবে।
ধরুন:
availableQuantity = 4
application restart-এর পরে যদি আবার:
100
হয়ে যায়, আমরা overselling করতে পারি।
Inventory is durable business state।
Order
Order-এর জন্য preserve করতে হবে:
OrderId
CustomerId
OrderStatus
OrderItems
কারণ customer পরে:
Order history
দেখবে।
Payment বা cancellation-এর সময় existing Order আবার load করতে হবে।
OrderItem
OrderItem historical business record-এর অংশ।
আমাদের preserve করতে হবে:
ProductId
quantity
purchase-time unit price
কারণ current Product price পরে change হলেও historical Order total change হওয়া যাবে না।
Why Product Price Alone Is Not Enough
Suppose Product:
Keyboard
price = 100
Customer creates an Order:
2 × Keyboard
Order total:
200
এক সপ্তাহ পরে admin changes Product price:
price = 120
যদি historical Order read করার সময় আমরা current Product price ব্যবহার করি:
2 × 120 = 240
এটি ভুল।
তাই OrderItem persist করে:
unitPrice = 100
at purchase/order creation time।
Persistence supports our earlier domain decision:
Historical Order price must remain stable even when current Product price changes.
What Does "Persist" Actually Mean?
Persistence means শুধু:
save object somewhere
না।
আমাদের system-এ persistence operations include:
store
retrieve
update
query
delete where appropriate
For example:
save Product
find Product by ProductId
find orderable Products
load Inventory by ProductId
update Inventory quantity
save Order
find Order by OrderId
find Orders by CustomerId
Why PostgreSQL?
Our current engineering context already selected:
PostgreSQL
as the relational database।
This fits our system well because our data has clear relationships:
Product
↓
Inventory
Order
↓
OrderItems
Order
↓
CustomerId
and we need strong consistency for workflows such as:
Create Order
where multiple related state changes must succeed together।
Relational Data Fits Our Domain
Consider Order:
Order
├── OrderItem
├── OrderItem
└── OrderItem
This naturally maps to relational structures such as:
orders
order_items
where rows can be connected through keys।
Likewise Inventory has a clear relationship to Product।
Relational storage gives us mechanisms such as:
primary keys
foreign keys
constraints
transactions
indexes
which will become useful in later lessons।
Database Is Not Just a File
A production database provides more than disk storage।
It helps with:
durability
concurrent access
querying
transactions
data integrity
These properties are crucial for a multi-request backend।
Multiple Requests Access the Same State
Our application can receive concurrent requests:
Customer A creates Order
Customer B creates Order
Admin updates Inventory
Customer C browses Products
They may all interact with the same data।
A relational database provides a coordinated shared state store।
Without this, each application request cannot safely assume it is the only one modifying state।
Persistence Changes How We Think About Objects
So far we could imagine:
Product product = ...
existing indefinitely।
With persistence, a typical flow becomes:
HTTP Request
↓
UseCase
↓
Repository
↓
Database
Repository loads data:
Database
↓
Product
UseCase/domain operates on it:
Product
↓
business behaviour
Then changes are persisted again।
Example: Product Update
Conceptually:
PATCH /products/P-100
↓
UpdateProductHandler
↓
UpdateProductUseCase
↓
ProductRepository.findById(P-100)
↓
Product
↓
product.changePrice(...)
↓
ProductRepository.save(product)
The domain object still owns its behaviour।
Persistence provides durable storage before and after that behaviour।
Example: Create Order
Our accepted workflow:
identify Customer
load Products
check Product state
load Inventory
check quantity
capture current prices
decrease Inventory
create Order
persist Order
Now persistence becomes essential।
Conceptually:
CreateOrderUseCase
↓
ProductRepository
↓
InventoryRepository
↓
OrderRepository
all backed by PostgreSQL।
Persistence Does Not Replace the Domain
A dangerous mindset is:
database table
↓
Java class
↓
API
where database schema becomes the entire application model।
We deliberately avoid that।
Our direction remains:
business requirements
↓
domain model
↓
application workflows
↓
persistence design
Database exists to persist application state।
It does not define the business by itself।
Entity Does Not Mean Database Row
We already learned:
Domain Entity
means:
an object with identity and lifecycle
For example:
Order
is a domain Entity।
Later JPA will also use the annotation:
@Entity
That is a persistence concept।
These meanings overlap sometimes, but they are not identical।
Avoid Database-First Domain Modeling
Suppose we begin with:
CREATE TABLE orders (
...
);
then generate Java classes directly from the schema and call that the domain model।
This can produce objects focused on:
columns
foreign keys
getters/setters
instead of:
cancel()
markPaid()
total()
Our approach is the opposite:
understand business behaviour first
then design persistence that supports it
Database Responsibilities
PostgreSQL should help us with:
durable storage
atomic persistence
relationships
structural integrity
efficient querying
concurrent data access
These are legitimate database responsibilities।
Database Is Not the Business Workflow
PostgreSQL should not become the place where all application behaviour lives।
For example, avoid designing the entire Order lifecycle as obscure database triggers such as:
when order status changes
automatically update inventory
automatically call payment system
This hides application behaviour outside the Java workflow।
Our application architecture remains:
Handler
↓
UseCase
↓
Domain
↓
Repository
↓
Database
UseCase Owns Workflow
For cancellation:
CancelOrderUseCase
coordinates:
load Order
verify ownership
cancel Order
restore Inventory
persist changes
Database participates in storing these changes atomically।
It does not replace the UseCase।
Domain Owns Invariants
For example:
Inventory quantity cannot be negative
should remain protected by:
inventory.decrease(...)
and:
inventory.setAvailableQuantity(...)
Database can later reinforce this with a constraint।
But domain behaviour remains explicit।
Database Constraints Are Reinforcement
Suppose Inventory has:
available_quantity >= 0
We may later add a database constraint enforcing that too।
Why?
Because it protects persisted integrity even if application code has a bug or another code path writes invalid data।
This creates layered protection:
request validation
↓
domain invariant
↓
database constraint
Each layer protects a different boundary।
Database Is the Last Integrity Boundary
Imagine a bug accidentally tries to persist:
availableQuantity = -3
Even if application validation failed somewhere, database constraint can reject the invalid row।
This is valuable।
But we don't say:
Since the database has a CHECK constraint, the domain no longer needs validation.
Both remain useful।
What Should Not Be Persisted?
Not every Java object needs a database table।
Examples:
CreateOrderRequest
ProductResponse
ApiProblem
PageResponse
These are transport objects।
They are temporary representations around a request/response boundary।
They do not represent durable business state।
DTOs Do Not Need Tables
Avoid creating tables like:
create_order_requests
api_problem_responses
product_response
simply because those Java classes exist।
Persistence is driven by durable business state, not class count।
UseCases Do Not Need Tables
Likewise:
CreateOrderUseCase
CancelOrderUseCase
are application behaviour।
They are not data entities।
No database table is needed for a UseCase।
Handlers Do Not Need Persistence
OrderHandler should not contain:
database state
or request history as instance fields।
Handler is an HTTP adapter।
Persistence remains behind Repository boundaries।
Payment Is More Nuanced
Our Payment capability will eventually integrate with an external provider।
Do we need a:
payments
table?
Maybe।
But we explicitly deferred that decision until the integration contract requires it।
We might need to persist:
provider payment reference
attempt state
idempotency data
or perhaps less।
Do not create a Payment table merely because Payment exists as a capability।
Customer Is Also Important
We have a:
CustomerId
but no local Customer aggregate in v1।
Therefore we should not create:
customers
table automatically।
Our Order can persist:
external CustomerId
directly।
Again:
A domain noun does not automatically require a local table.
Product and Inventory Are Separate
We deliberately modeled:
Product
and:
Inventory
as separate responsibilities।
Persistence should respect that conceptual separation।
That likely means distinct persisted structures for:
Product catalog state
and:
available quantity
rather than stuffing Inventory into Product simply because SQL can store both in one row।
Why Separation Matters
Product price changes because:
commercial/catalog decision
Inventory changes because:
orders
cancellations
admin stock adjustments
These have different change patterns and invariants।
Keeping persistence aligned with responsibility boundaries improves clarity।
Persistence Does Not Mean Microservices
We may later have tables such as:
products
inventory
orders
order_items
That does not imply:
Product Service
Inventory Service
Order Service
as separate deployments।
We still have:
one modular monolith
one Spring Boot application
one PostgreSQL database
Logical separation is not network separation।
One Database Is Fine
For our current architecture:
one PostgreSQL database
is a deliberate pragmatic choice।
It supports local transactions across:
Inventory
Order
which is valuable for Order creation and cancellation।
We do not need separate databases per capability।
Local Transaction Example
Successful Order creation requires:
Inventory decreases
Order is saved
These should behave as one logical operation।
We don't want:
Inventory decreased
but:
Order failed to persist
leaving inconsistent state।
PostgreSQL transactions will help us make these local changes atomic।
We cover this deeply later in Module 7।
Persistence Failure Is Possible
Once a database exists, new failure modes appear:
database unavailable
connection failure
transaction failure
constraint violation
deadlock
timeout
Backend engineers need to think about these failures explicitly।
Persistence is not an infallible object store।
Repository Boundary
Our architecture already has:
Handler
↓
UseCase
↓
Repository
Repository represents the application's persistence/query needs।
For Product:
ProductRepository
may conceptually support operations such as:
findById
save
plus required product queries।
Repository Is Not Automatically Generic CRUD
Avoid starting with:
interface Repository<T, ID> {
T save(T entity);
T findById(ID id);
List<T> findAll();
void delete(ID id);
}
for every domain object simply because CRUD is easy to generalize।
Our domain does not even support physical Product deletion।
A generic repository can accidentally suggest operations that the business doesn't allow।
Repository Should Reflect Application Needs
For example, Product application needs:
find Product by ID
persist Product
browse orderable Products
Inventory needs:
find by ProductId
persist changed quantity
list Inventory for admin
Order needs:
save Order
find accessible Order
find customer Order history
Actual interfaces should emerge from these workflows।
Don't Put SQL in UseCases
Avoid:
createOrderUseCase.execute() {
entityManager.createQuery(...);
}
or:
jdbcTemplate.query(...)
inside UseCase।
UseCase coordinates business operation।
Repository/persistence layer owns database mechanics।
Don't Put HTTP in Repository
Likewise Repository should not:
return ResponseEntity
throw HTTP 404
build ApiProblem
Persistence reports data/result/failure in application terms।
HTTP mapping remains at the edge।
Persistence Representation May Differ From Domain
Suppose domain Order stores:
List<OrderItem>
Persistence may use:
orders row
+
multiple order_items rows
That's fine।
Domain representation and storage representation solve different problems।
Mapping connects them।
Object Graph vs Relational Tables
In Java we think:
Order
↓
List<OrderItem>
In relational storage we may think:
orders
↑
order_items.order_id
This difference is often called:
object-relational impedance mismatch
We don't need to memorize the term।
The practical point is:
Java object relationships and relational data relationships are not identical representations.
JPA helps bridge them, but we still need deliberate mapping।
Persistence Needs Stable Identity
To retrieve state later, records need identity।
Our domain already has:
ProductId
OrderId
CustomerId
Inventory is identified by:
ProductId
These concepts will influence schema keys later।
The exact database key representation will be decided during schema design।
Do Not Choose IDs Accidentally From JPA
A common mistake is:
@Id
@GeneratedValue
private Long id;
then deciding:
Our domain uses Long IDs because Hibernate generated that for us.
Wrong direction।
We should decide identifier semantics deliberately, then map them to persistence।
The ORM should not define our domain identity strategy accidentally।
Persistence and Historical Data
Persistence is not only about current state।
Order represents history।
For example:
current Product price
and:
OrderItem unitPrice
may intentionally differ।
Database schema must preserve both concepts։
Never "normalize away" historical data if doing so destroys business meaning।
Not Every Duplication Is Bad
In relational design, engineers often try to avoid duplicate values।
But storing:
OrderItem.unitPrice
even though Product also has:
Product.price
is intentional historical duplication।
Because they mean different things:
Product.price
→ current price
OrderItem.unitPrice
→ price used when Order was created
Same numeric type, different business meaning।
Persistence and Deactivation
Product deactivation means:
active = false
or equivalent persisted lifecycle state।
We do not physically delete the Product because historical Orders may refer to it।
This is another example where business semantics determine persistence behaviour।
Why Physical Delete Can Be Dangerous
Suppose OrderItem references:
Product P-100
Admin deactivates Product।
If implementation does:
DELETE FROM products
WHERE id = 'P-100';
we may:
break foreign-key relationships
lose useful historical catalog reference
or need awkward cascading behaviour।
Our requirement is deactivation, not deletion।
Persistence should implement the actual lifecycle։
Order Cancellation Also Persists Two Changes
Cancellation requires:
Order status → CANCELLED
and:
restore Inventory
Both must survive restart।
If only Order status persists but Inventory restoration does not, data becomes inconsistent।
Persistence design must support the complete business workflow।
Payment State Must Survive Too
Once an Order is successfully paid:
status = PAID
must persist।
Otherwise a restart could make the application think the Order is still:
UNPAID
and allow another payment attempt।
This becomes especially important when we discuss external-service idempotency later।
Persistence Is Not a Cache
PostgreSQL is the authoritative durable state for our application-owned business data।
It is not merely:
a performance cache
If application memory disagrees with PostgreSQL after restart, PostgreSQL wins for persisted application state।
We Don't Need Redis Yet
Could Redis store some of this state?
Technically many systems use Redis for various purposes।
But our requirements do not need it।
Adding:
Redis
would introduce:
another datastore
consistency questions
operational complexity
without solving a current problem better than PostgreSQL।
Our architecture remains intentionally simple।
We Don't Need Event Sourcing
Another possible persistence style is storing every domain event and reconstructing state।
That would introduce:
event store
replay
event schemas
versioning
projection logic
Our system does not require this complexity।
We persist current business state relationally।
We Don't Need CQRS
Read and write operations can use the same relational persistence foundation।
We may use efficient read projections later where useful।
That does not require a separate:
CQRS architecture
or separate databases।
PostgreSQL as a Shared Transaction Boundary
One major advantage of our modular monolith is that related local state lives in one database।
For:
CreateOrderUseCase
we can eventually perform:
decrease Inventory
insert Order
insert OrderItems
within one database transaction।
This is far simpler than coordinating multiple remote services/databases।
This Is an Architectural Benefit
Sometimes engineers split systems into microservices early and later discover they need distributed transactions for workflows that were naturally local։
Our current system avoids that problem।
We can preserve:
strong local consistency
for Order and Inventory without distributed coordination।
Database State vs External Payment State
However Payment Provider is external।
We cannot put:
our PostgreSQL transaction
around:
external provider's database
That means payment introduces a different consistency problem।
We already recognized:
local DB changes
+
external payment execution
cannot be one normal PostgreSQL transaction।
That complexity belongs in the external-services module।
Persistence Boundaries Help Reveal This
Local operations:
Create Order
Cancel Order
can be atomic within PostgreSQL।
Cross-system operation:
Pay Order
cannot rely on one local transaction for the entire external interaction։
Understanding persistence boundaries helps us distinguish these cases early।
Data Must Be Queryable
Persistence isn't useful if we can only save data but cannot efficiently retrieve what the API needs।
Our known query patterns include:
find Product by ID
browse orderable Products
find Inventory by ProductId
list Inventory
find Order by ID
find Orders by CustomerId
paginate Orders
These query patterns will influence:
schema
indexes
Repository methods
later।
Design Schema From Access Patterns Too
A relational schema should preserve domain meaning, but we also need to understand how the application accesses the data।
For example:
GET /orders
needs:
CustomerId scope
bounded pagination
stable ordering
This will later influence indexing choices।
Schema design is not isolated from API/usecase behaviour।
Don't Index Everything
Once we learn about indexes, it may be tempting to add them to every column।
Indexes have costs:
disk usage
write overhead
maintenance
We will add them based on actual query patterns।
Our current API design gives us those patterns।
Persistence Has Operational Responsibility
Once PostgreSQL becomes part of the application, engineers also need to care about:
database migrations
connection configuration
backups
monitoring
query performance
Not all of these are covered deeply in this course, but production backend engineering must recognize them।
Schema Must Evolve Safely
Production databases contain real state।
We cannot simply:
drop everything
recreate tables
for every model change।
That's why we'll use:
Flyway
for versioned database migrations।
Application code evolves։
Database schema must evolve in a controlled way with it।
ddl-auto=create Is Not Our Migration Strategy
Spring/JPA can be configured to generate schema automatically।
That can look convenient during experiments।
But for our production-style application, we want database changes to be:
explicit
reviewable
versioned
repeatable
Flyway will own schema migrations։
JPA mappings will map to the schema, not silently replace migration discipline।
Persistence Is Part of Definition of Done
When a ticket changes durable data requirements, Definition of Done may include:
database migration
Repository changes
integration tests
backward-safe schema evolution
A feature is not complete if Java code expects a column that production database does not have।
Local Development
During local development, PostgreSQL will run as a supporting dependency।
Our established workflow:
docker compose up -d
starts dependencies such as PostgreSQL।
Spring Boot application runs normally via:
./gradlew bootRun
or IDE/JAR।
We don't need to containerize the application itself yet।
Automated Tests
Integration tests should not depend on the developer manually running local Compose।
Later we'll use:
Testcontainers
to start controlled PostgreSQL instances for automated tests।
That makes tests:
repeatable
isolated
CI-friendly
Why Not H2 for Everything?
Some Spring applications use an in-memory database such as H2 for tests।
But relational databases differ in:
SQL behaviour
data types
locking
constraints
query planner behaviour
For PostgreSQL-specific integration confidence, testing against real PostgreSQL through Testcontainers is stronger।
Unit tests still do not need a database।
Not Every Test Needs PostgreSQL
We should not turn every domain test into an integration test।
Example:
order.cancel();
can be tested with plain Java।
No database needed।
Similarly:
inventory.decrease(...)
is domain behaviour।
Use PostgreSQL integration tests for persistence/query/transaction behaviour where the database actually matters।
Persistence Testing Layers
A balanced strategy later:
Domain test
→ no DB
UseCase unit test
→ test doubles where appropriate
Repository integration test
→ PostgreSQL Testcontainer
API integration test
→ Spring + PostgreSQL for key workflows
Each test type proves a different concern।
Avoid Mocking PostgreSQL Behaviour
A mock Repository cannot prove:
JPA mapping works
foreign keys work
query returns correct rows
transaction behaves correctly
migration is valid
That's why persistence needs real integration testing eventually।
Repository Is a Boundary Worth Testing
For example:
OrderRepository
may have an integration test ensuring:
save Order with items
reload Order
historical prices preserved
CustomerId preserved
status preserved
That verifies mapping between:
domain/application
and:
PostgreSQL
works correctly।
Persistence Should Be Boring
A good persistence layer usually should not be the most clever part of the system।
We want:
clear schema
clear mappings
predictable queries
explicit migrations
measured indexes
not:
magic triggers
generic repositories everywhere
reflection-heavy persistence abstractions
clever dynamic SQL frameworks without need
Simple persistence is easier to operate։
Example State Before Restart
Suppose PostgreSQL contains:
Product
P-100
Keyboard
100.00
active
Inventory
P-100
4
Order
O-1001
Customer C-10
UNPAID
OrderItem
P-100
quantity 2
unitPrice 100.00
Application restarts।
After restart, Repository loads:
same business state
That's the core value of persistence।
Example State Evolution
Admin changes Product price:
100 → 120
Now persistence contains:
Product.currentPrice = 120
But historical OrderItem remains:
unitPrice = 100
Application can now correctly answer both:
What does this Product cost now?
What did this customer order it for?
Persistence preserves business history and current state simultaneously।
Example Cancellation
Before cancellation:
Order O-1001
status = UNPAID
Inventory P-100
quantity = 4
Order has:
2 units of P-100
Cancellation workflow:
Order.status
UNPAID → CANCELLED
Inventory
4 → 6
Both changes persist together।
After restart:
Order remains CANCELLED
Inventory remains 6
That's durable consistency।
Persistence Review Questions
Before storing something, ask:
Does this state need to survive application restart?
Does the application own this state?
Is it current state or historical state?
What identifies it?
What workflows modify it?
What queries need it?
What integrity rules should the database reinforce?
Does it really need its own table?
These questions prevent both under-modeling and over-modeling।
What We Currently Expect to Persist
Our current likely durable model:
Product
Inventory
Order
OrderItem
with Order storing:
external CustomerId
No local full Customer aggregate/table required for v1।
Payment persistence will be introduced only if the integration workflow requires it।
What We Do Not Persist as Business Tables
Current examples:
CreateProductRequest
CreateOrderRequest
OrderResponse
ApiProblem
PageResponse
Handler
UseCase
These are application/transport constructs, not durable business records।
Common Mistake 1 — Everything Becomes a Table
Java classes and domain nouns do not automatically correspond one-to-one with database tables।
Common Mistake 2 — Schema Drives the Entire Domain
Business behaviour should still shape our domain model and workflows।
Common Mistake 3 — Generic CRUD Repository Everywhere
Repository APIs should reflect real application operations։
Common Mistake 4 — SQL Inside UseCase
Persistence mechanics belong behind Repository/persistence boundaries।
Common Mistake 5 — Database Owns Business Workflow
Triggers and stored logic should not invisibly replace application/domain behaviour without a real reason।
Common Mistake 6 — Domain Validation Removed Because Database Has Constraints
Database constraints reinforce invariants; they don't replace domain correctness।
Common Mistake 7 — Local Customer Table Added Automatically
External identity is authoritative; v1 only needs CustomerId for ownership।
Common Mistake 8 — Payment Table Added Before Integration Design
Persist only what the real payment workflow needs।
Common Mistake 9 — Product Deactivation Implemented as Physical Delete
Historical Orders must remain valid and referentially meaningful।
Common Mistake 10 — In-Memory Database Assumed Equivalent to PostgreSQL
Important persistence behaviour should eventually be integration-tested against PostgreSQL itself।
Responsibility Map
Handler
Understands:
HTTP
No direct SQL/JPA persistence mechanics।
UseCase
Coordinates:
application workflow
transaction intent
Uses Repositories।
Domain
Owns:
business state
local invariants
behaviour
No SQL/JPA requirements should drive its meaning।
Repository
Defines:
application persistence/query needs
and separates those needs from database implementation details।
PostgreSQL / Persistence Layer
Owns:
durable storage
relational mapping
queries
database constraints
transaction mechanics
indexes
Engineering Principle
The core principle:
Persistence exists to make business state durable; it should support the domain, not become the domain.
Another:
Use the database to reinforce integrity and coordinate durable state, while keeping business workflows explicit in UseCases and domain objects.
And:
Persist only state the application genuinely owns and needs to survive—not every class, DTO, or noun in the codebase.
Summary
In this lesson, we learned that:
- Java memory is temporary and cannot preserve production business state across application restarts.
- Product, Inventory, Order, and OrderItem contain durable state that must be persisted.
- OrderItem must preserve purchase-time price independently from current Product price.
- PostgreSQL is the durable relational datastore for our modular monolith.
- Persistence provides durability, querying, transactions, integrity, and coordinated concurrent access.
- Persistence should support the domain rather than drive domain modeling.
- Domain Entities and JPA entities are related concepts but not the same thing.
- Product and Inventory remain separate responsibilities in persistence as well as domain design.
- External Customer identity does not justify a local Customer table in v1.
- Payment persistence remains deferred until the actual integration contract requires it.
- Database constraints reinforce domain invariants but do not replace them.
- Handlers, UseCases, DTOs, API Problems, and pagination responses are not business tables.
- Repositories should reflect actual application persistence needs rather than expose generic CRUD by default.
- SQL/JPA mechanics belong behind Repository/persistence boundaries, not inside UseCases.
- Product deactivation must preserve historical references rather than physically deleting Product data.
- Local Order creation and cancellation benefit from one PostgreSQL transaction boundary.
- External Payment cannot be made atomic with PostgreSQL through one ordinary local transaction.
- API query patterns will later influence schema and index design.
- Flyway will manage explicit versioned database migrations.
- Automated persistence tests will use PostgreSQL with Testcontainers where appropriate.
- Domain and UseCase tests should not require PostgreSQL when persistence behaviour is irrelevant.
- Persistence should remain explicit, predictable, and operationally boring.
Next lesson:
Relational Database Thinking
There we will stop thinking in Java object graphs for a moment and learn how PostgreSQL sees our data—as relations, rows, keys, constraints, and joins—before designing the actual schema.