Production Change Request
Assessing the Impact on the Existing System
In the previous lesson, Product introduced a new production requirement:
Authorized administrators must be able to issue a full refund for an eligible paid Order through the existing Payment Provider, and the successful refund must become visible durable system state.
We deliberately did not begin by writing:
REFUNDED
or creating:
refunds
tables.
Now we need to understand what the requirement actually changes.
Our goal is:
Assess the refund requirement against the existing production system and classify each boundary as something that must change, may change, or should remain unchanged.
This is one of the most important differences between:
greenfield implementation
and:
production system evolution
Start From the Existing System
Before changing anything, recall what is already true.
Order lifecycle
UNPAID
├── payment success ──→ PAID
└── cancellation ─────→ CANCELLED
Current enum:
public enum OrderStatus {
UNPAID,
PAID,
CANCELLED
}
Historically:
PAID
and:
CANCELLED
were terminal because the initial scope had no refund requirement.
That was correct for v1.
It is no longer automatically correct for the new requirement.
Payment Is Already Separate From Order Lifecycle
This existing design decision matters enormously.
We deliberately separated:
Order lifecycle
from:
Payment attempt lifecycle
A Payment attempt may experience:
success
confirmed failure
timeout
unknown outcome
without adding:
PAYMENT_TIMEOUT
PAYMENT_FAILED
PAYMENT_PENDING
to OrderStatus.
That principle should guide refunds too.
We should not automatically add:
REFUND_PENDING
REFUND_FAILED
REFUND_TIMEOUT
to OrderStatus.
Those sound like external-operation states rather than Order business states.
Impact Classification
We will classify each major area as:
MUST CHANGE
MAY CHANGE
or:
SHOULD NOT CHANGE
This prevents us from touching unrelated parts of the system merely because the feature is significant.
1. Order Domain — MUST BE REVISITED
The Order Domain cannot remain completely untouched.
Why?
Today:
PAID
means:
payment successfully completed
no further OrderStatus transition supported
The new business requirement says:
a paid Order may later have
its successful payment refunded
That invalidates the old assumption:
PAID is always terminal
So the Order lifecycle must at least be reconsidered.
Does OrderStatus Definitely Need REFUNDED?
Not yet.
Two broad models are possible.
Option A — Refund changes OrderStatus
Conceptually:
UNPAID
├──→ PAID ──→ REFUNDED
└──→ CANCELLED
This makes:
REFUNDED
a clear business lifecycle state.
Potential benefit:
Customer reads Order
→ immediately sees REFUNDED
Potential cost:
existing API enum changes
database CHECK constraint changes
existing clients must understand new value
financial processing state risks leaking
into Order lifecycle
Option B — Order Remains PAID, Refund Is Separate Financial State
Conceptually:
Order.status = PAID
Payment / Refund state
= REFUNDED
This preserves:
Order lifecycle
as separate from:
financial operation history
Potential benefit:
clean separation between
commercial Order state
and financial processing state
Potential cost:
Customer-facing Order representation
needs another way to show refund outcome
Neither option should be chosen casually.
This is now a real design decision.
What Is Certain About the Domain?
Even before choosing representation, we know the business rule:
UNPAID
→ cannot be refunded
CANCELLED
→ cannot be refunded
successfully PAID
→ may be eligible for refund
and:
successfully fully refunded payment
→ cannot be fully refunded again
Some domain/application boundary must protect those rules.
Should Order Own Refund Eligibility?
Probably some part of refund eligibility belongs to Order business state.
For example:
is this Order PAID?
is clearly an Order lifecycle question.
But:
has provider refund already succeeded?
is there an unresolved refund attempt?
belongs to financial workflow state.
So eligibility will likely combine:
Order state
+
Refund/Payment workflow state
at the application layer.
This mirrors existing Payment design.
2. Existing markPaid() — SHOULD NOT CHANGE
The new requirement does not change what successful payment means.
Existing rule remains:
UNPAID
→ markPaid()
→ PAID
We should not weaken it into:
UNPAID
→ PAID
REFUNDED
→ PAID
or allow:
PAID
→ PAID
arbitrarily.
Refund support should add a new responsibility rather than corrupt existing payment semantics.
3. Cancellation — SHOULD NOT BECOME REFUND
Current cancellation:
UNPAID
→ CANCELLED
and:
Inventory restored
The refund requirement does not redefine cancellation.
Therefore this remains invalid:
PAID
→ cancel()
We should not make:
CancelOrderUseCase
perform financial refunds.
That would merge:
unpaid Order cancellation
with:
paid financial reversal
which are different workflows.
Cancellation API Should Remain Stable
Existing:
POST /api/v1/orders/{orderId}/cancel
continues to mean:
cancel eligible UNPAID Order
It should not suddenly mean:
cancel or maybe refund depending on status
That would make API behavior ambiguous and harder to reason about.
Refund deserves its own explicit application operation.
4. Inventory — SHOULD NOT CHANGE
The new requirement explicitly says:
refund
does not automatically restore Inventory
Therefore refund workflow must not call:
InventoryRepository.restore(...)
Existing Inventory rules remain unchanged.
This is important because a financial refund is not equivalent to:
physical product returned to stock
Why This Boundary Protects Future Changes
If Product later asks for:
returns
that requirement might introduce:
return received
↓
inspect item
↓
restock eligible quantity
That would be a separate workflow.
Keeping refund independent now prevents us from encoding false assumptions.
5. Product — SHOULD NOT CHANGE
Refund eligibility must not depend on:
current Product active state
or:
current Product price
Suppose:
Order O1 paid at priceCents = 5000
and Product is later:
inactive
or price changes to:
7000
O1's refund must still be based on the historical paid Order/payment.
Therefore:
ProductRepository
should not become a required collaborator of the refund workflow.
That would be a warning sign.
Historical Pricing Becomes Valuable Again
OrderItem.unitPriceCents already preserves:
purchase-time price
That means the Order total remains historically stable.
Refund logic must not reconstruct the amount from current Product data.
This earlier design decision makes the new production change easier.
6. Money Representation — SHOULD NOT CHANGE
We already use:
long cents
with fields such as:
unitPriceCents
totalCents
The new requirement introduces no:
partial refunds
multi-currency
fractional refund allocation
So we do not need a new generic Money framework.
A full refund amount can remain expressed in integer minor units.
7. Payment Capability — MUST CHANGE
This is the most obvious impact.
The current external boundary conceptually supports:
pay(...)
The system now needs:
refund(...)
or another provider-neutral way to express the new external financial capability.
We must not call the provider directly from:
RefundOrderUseCase
with provider-specific HTTP details.
The existing principle remains:
UseCase
↓
external Service boundary
↓
provider implementation
Do We Extend PaymentService?
This is an open design question.
For example:
public interface PaymentService {
PaymentResult pay(...);
RefundResult refund(...);
}
could be reasonable because both operations target the same external payment capability.
But we should assess whether:
payment execution
and:
refund execution
share a coherent provider-neutral boundary.
We should not create:
RefundService
automatically just because the method name differs.
Remember our convention:
Service
represents external capability boundaries.
The abstraction should reflect real external responsibility.
8. Payment Provider Adapter — MUST CHANGE
Whatever boundary we choose, the concrete provider integration needs refund support.
That includes:
HTTP request
provider authentication
timeout behavior
response mapping
error translation
But exact implementation is blocked on provider capability research.
We currently do not know enough to write trustworthy provider DTOs.
Provider-Specific Questions Are Blocking
Before implementation we need to know:
Does the provider support full refund?
Which Payment/provider reference is required?
Does the provider require the original payment ID?
Does it support idempotency?
Can a refund be queried?
What is a confirmed rejection?
What happens when request times out?
Does provider return a Refund ID?
These are not academic questions.
They determine consistency design.
9. PaymentAttempt — MUST BE REVIEWED, NOT REUSED BLINDLY
The current system already has durable Payment-attempt concepts because payment can produce:
success
failure
unknown outcome
and needs:
duplicate execution protection
provider references
reconciliation
Refund has similar failure characteristics.
That does not automatically mean:
PaymentAttempt
should be reused for refunds.
Why Reusing PaymentAttempt May Be Wrong
A Payment attempt means conceptually:
try to charge an unpaid Order
A Refund attempt means:
try to return money
for a previous successful payment
They have different:
eligibility rules
external operation
direction of money movement
lifecycle meaning
provider references
Forcing both into one generic record may make the model vague.
Why a Separate RefundAttempt May Be Reasonable
A separate concept could own:
RefundAttemptId
OrderId
original successful payment reference
provider refund reference
refund amount
attempt state
timestamps
But those fields are not yet finalized.
We need provider semantics first.
The current impact conclusion is simply:
refund needs durable external-operation state
not:
create this exact schema now
10. External Side-Effect Strategy — MUST REUSE EXISTING PRINCIPLES
The existing Payment design taught us:
never hold a DB transaction open
while waiting for remote provider
That rule still applies.
Do not design:
BEGIN TRANSACTION
lock Order
call provider refund
(wait 5 seconds)
update DB
COMMIT
This would hold database locks across network latency and failure.
Refund Needs Multi-Phase Coordination
At a high level, the workflow will likely need:
local decision / durable attempt
↓
COMMIT
↓
provider refund call
↓
local completion / reconciliation
This mirrors the consistency problem already solved conceptually for Payment.
Exact fields and states come later.
11. Transaction Boundaries — MUST CHANGE FOR NEW WORKFLOW
Refund cannot be:
one PostgreSQL transaction
because provider execution is external.
Therefore we need at least:
short local transaction before provider call
and:
short local transaction after provider result
with durable state connecting them.
This is different from:
Create Order idempotency
which was entirely local and could be one transaction.
Compare Create Order vs Refund
Create Order:
PostgreSQL only
so:
claim
+
Inventory
+
Order
can commit atomically.
Refund:
PostgreSQL
+
External Payment Provider
so:
local DB
cannot atomically commit with:
provider side effect
Different failure model.
Different design.
12. Unknown Outcome Handling — MUST BE ADDED
The new workflow must explicitly handle:
refund call timed out
as:
outcome unknown
not automatically:
refund failed
Why?
Because the provider may already have returned money.
Blindly retrying could create:
double refund
depending on provider semantics.
Unknown Outcome Blocks Certain Actions
Once a refund attempt is unresolved, what should another administrator request do?
We should not allow:
start a completely new refund
without first resolving whether the previous one succeeded.
Therefore likely invariant:
At most one unresolved refund operation may exist for an Order/payment at a time.
This mirrors our Payment coordination principle.
Exact persistence enforcement comes later.
13. Refund Idempotency — MUST BE DESIGNED
We learned from the Create Order incident that side-effecting retry safety needs:
logical operation identity
Refund needs the same careful reasoning.
But we should not blindly reuse:
Idempotency-Key
from Create Order without considering provider semantics.
There are at least two layers:
our application refund operation identity
and:
provider idempotency / provider refund identity
These must align safely.
14. Order Persistence — MAY CHANGE
If we decide:
Order.status gains REFUNDED
then persistence definitely changes.
Current database constraint conceptually allows:
UNPAID
PAID
CANCELLED
A new OrderStatus would require a Flyway migration updating the CHECK constraint.
But if refund state remains entirely separate:
orders.status
may remain unchanged.
So Order table impact depends on the lifecycle design.
Existing Schema Makes Evolution Manageable
We used:
TEXT + CHECK
rather than PostgreSQL native enum.
That means adding a new lifecycle value, if chosen, can be performed through a normal migration.
Earlier simplicity improves changeability now.
15. New Persistence — MUST CHANGE SOMEWHERE
Regardless of OrderStatus decision, we need durable refund workflow information.
Why?
Because we must survive:
application restart
provider timeout
provider success + local failure
duplicate admin request
Pure in-memory state cannot do that.
Therefore some new persistence is unavoidable.
But Do Not Decide the Table Yet
We should not immediately create:
refunds (...)
without deciding:
what is a Refund?
what is a RefundAttempt?
can there be multiple attempts?
what is the provider identity?
what states exist?
what must be reconciled?
Impact analysis tells us persistence changes.
Design determines the schema.
16. Historical Data — MUST BE SUPPORTED
This requirement applies to:
existing PAID Orders
not only new Orders created after deployment.
That means migration/design must not depend on new creation-time refund metadata that historical Orders do not possess.
We must verify that existing successful Payment state contains enough information to identify the provider transaction that can be refunded.
Historical Payment Reference Is Critical
A refund typically needs to refer back to:
the successful provider payment
If existing paid Orders do not persist the required provider reference, the change becomes more difficult.
That is one of the first things we must inspect in the next design/investigation step.
Possible outcomes:
existing reference is sufficient
or:
historical provider data must be recovered
or:
some historical Orders cannot be safely automated
without additional operational handling
We must not pretend we know before checking.
17. API Command Surface — MUST CHANGE
Administrators need a way to initiate the workflow.
Therefore a new API operation is required.
We have not finalized the path, but it should likely fit existing business-action conventions.
Current examples:
POST /api/v1/orders/{orderId}/pay
POST /api/v1/orders/{orderId}/cancel
Refund should be similarly explicit.
Exact endpoint design belongs to the next design lesson.
18. Customer Read API — MUST CHANGE SOMEHOW
Product says:
Customer can see that payment was refunded.
Therefore existing Order reads need to expose refund information somehow.
This affects:
GET /api/v1/orders
and/or:
GET /api/v1/orders/{orderId}
depending on response design.
But We Must Protect Compatibility
Existing clients already understand:
UNPAID
PAID
CANCELLED
Adding:
REFUNDED
to the same enum may be a breaking change for clients using exhaustive enum handling.
An additive optional field may be safer in some environments.
We need to evaluate the actual client contract.
Example Compatibility Question
Existing client:
switch status:
UNPAID
PAID
CANCELLED
If server suddenly returns:
REFUNDED
what happens?
Possible client behavior:
crash
deserialize failure
unknown status fallback
safe default
We must not assume.
Production API evolution includes consumer behavior.
19. API Version — MAY CHANGE, BUT NOT AUTOMATICALLY
A new requirement does not automatically require:
/api/v2
If we can evolve the v1 contract compatibly, keeping:
/api/v1
may be correct.
If the only reasonable representation is breaking for existing consumers, versioning may become justified.
We need evidence before deciding.
20. Problem Details — MUST BE EXTENDED
Refund introduces new business conflict categories.
Likely examples include:
Order not refundable
Refund already completed
Refund currently unresolved/in progress
But exact public codes are not finalized yet.
We should not invent all names in impact analysis.
The important conclusion is:
central RFC 7807 mapping
must cover new refund workflow failures
Existing Error Principles Stay
Still:
400
→ malformed transport/request
401
→ unauthenticated
403
→ insufficient authority
404
→ missing resource according to security contract
409
→ valid request conflicts with current business state
500
→ unexpected system failure
Refund should fit this model.
21. Security — MUST CHANGE
A new privileged endpoint needs authorization.
Requirement:
administrator only
Therefore security configuration/tests must explicitly protect the refund operation.
We should not rely only on:
frontend hides button
or:
Handler manually checks a string role
outside established security patterns.
Customer Ownership Still Does Not Authorize Refund
A Customer may own:
Order O1
That allows them to:
view their Order
It does not grant:
refund authority
Ownership and administrative permission remain distinct concepts.
22. Customer Identity Model — SHOULD NOT CHANGE
We still do not need:
Customer entity
CustomerRepository
Refund concerns an existing Order and administrator authority.
No local Customer aggregate is necessary.
The existing external CustomerId remains enough for ownership/read behavior.
23. Observability — MUST CHANGE
Refund is a financial workflow.
We need operational visibility into:
refund requested
provider call initiated
refund confirmed successful
refund confirmed rejected
refund outcome unknown
refund reconciled
Structured logging must make these states understandable.
Correlation Still Applies
Each HTTP request still gets:
requestId
Refund operation may also need its own durable:
RefundAttemptId
or equivalent.
These identities serve different purposes, just like:
requestId
and:
PaymentAttemptId
do today.
Metrics — MAY CHANGE
Useful bounded metrics may eventually include:
refund attempts
refund success
refund confirmed failure
refund unknown outcome
But do not add:
OrderId
CustomerId
RefundAttemptId
as metric labels.
Detailed identity belongs in logs.
24. Health Checks — SHOULD NOT CHANGE BY DEFAULT
The Payment Provider is an external runtime dependency.
Should readiness fail whenever provider refund capability is unavailable?
Probably not automatically.
Existing health semantics should remain focused on whether the application can safely serve normal traffic.
A temporary provider issue may affect:
refund
without making the entire application incapable of:
browse
order history
product management
Do not expand readiness without a deliberate operational requirement.
25. CI — SHOULD NOT FUNDAMENTALLY CHANGE
The existing CI pipeline already runs:
build
tests
PostgreSQL integration tests
container verification
Refund tests should join the same pipeline.
No new CI platform is required.
26. Test Strategy — MUST EXPAND
Refund introduces new tests across several layers.
Domain/application
UNPAID cannot refund
CANCELLED cannot refund
eligible PAID Order can begin refund workflow
already refunded cannot refund again
depending on final lifecycle representation.
Provider Boundary Tests
Need controlled HTTP/provider stub scenarios for:
success
confirmed rejection
timeout
connection failure
malformed response
unknown outcome
duplicate/idempotency behavior
No real production Payment Provider in automated tests.
Persistence/Transaction Tests
Need real PostgreSQL tests for:
durable refund attempt
one unresolved attempt invariant
local completion
rollback
concurrent admin requests
depending on final schema.
Concurrency Tests
At minimum:
two administrators
attempt full refund
for same Order
must not produce:
two independent provider refund executions
This will require real concurrency reasoning, not just Domain checks.
Recovery Tests
We will need to model:
provider refund succeeds
local state application fails
and prove:
system does not issue a second refund blindly
This is likely the highest-risk scenario.
27. Deployment/Migration — MUST BE PLANNED
Because this is production, we must ask:
Can migration deploy before code?
Can old code run against new schema?
Can new code read old rows?
Does API rollout need frontend coordination?
Can feature be enabled safely?
These questions did not matter as much in early greenfield lessons.
They matter now.
Prefer Additive Migration
Where practical:
add table
add nullable/additive field
add new constraint carefully
is safer than:
drop old structure
rewrite all historical data
require immediate synchronized deployment
But exact migration design waits for final modeling.
28. Documentation — MUST CHANGE
We will eventually need to update:
OpenAPI
refund API behavior
provider integration design
Order lifecycle documentation
possibly RFC/ADR
depending on final design significance.
A new financial workflow is substantial enough that architecture/design documentation should remain traceable.
29. Backlog — MUST EVOLVE
The original backlog stopped at:
BACKEND-128
Then production added:
BUG-201
Now Product introduces another set of work.
We should not make one enormous ticket:
Add refunds
The impact map suggests separate bounded work.
But ticket creation belongs after enough design is complete.
Must Change / May Change / Must Not Change
We can now summarize.
MUST CHANGE
Payment provider integration capability
refund application workflow
durable financial-operation persistence
administrator API surface
administrator authorization rules
customer/support read visibility
error mapping
testing
observability
migration/deployment planning
technical documentation
MUST BE REVISITED
Order lifecycle
OrderStatus representation
existing PaymentAttempt model
API compatibility
historical provider payment references
PaymentService boundary
These areas definitely need analysis, but the final implementation is not decided.
SHOULD NOT CHANGE
Product Domain
current Product price behavior
Inventory quantity behavior
unpaid cancellation semantics
Create Order idempotency semantics
Customer ownership model
Customer identity boundary
money representation in cents
Handler → UseCase → Repository architecture
PostgreSQL as local source of truth
Payment provider isolation
This is just as important as knowing what must change.
Preliminary Dependency Map
Conceptually:
Admin HTTP request
↓
Security
↓
Refund Order UseCase
↓
Order
↓
Refund workflow persistence
↓
Payment external boundary
↓
Payment Provider
Then after provider result:
Payment Provider result
↓
Refund workflow
↓
durable refund state
↓
Order/customer read model
The exact classes and transactions are not yet designed.
What We Learned From Existing Boundaries
Several earlier design decisions are now paying off.
Historical Order pricing
Because OrderItem stores:
unitPriceCents
we do not depend on current Product price.
Separate Payment boundary
Because provider HTTP is isolated, we can extend financial capability without putting provider code into Order.
Payment state separate from OrderStatus
Because we did not add payment failures/timeouts to OrderStatus, we have room to model refund processing without automatically turning Order lifecycle into external-provider telemetry.
Modular monolith
We can add the workflow inside the existing application without creating another service.
PostgreSQL
We already have a durable coordination store for refund attempt state.
Production Evolution Tests Architecture Quality
Earlier we said:
optimize for changeability
not:
design every future feature
Refunds are now testing that decision.
We did not build refund functionality prematurely.
But our boundaries should make it possible to add now without rewriting:
Product
Inventory
Order creation
security model
entire application architecture
That is the value of good boundaries.
What We Still Need Before Designing the Workflow
The biggest unanswered question is:
What exactly does the Payment Provider support for refunds?
We need real provider semantics before finalizing:
RefundAttempt state model
idempotency strategy
provider reference fields
reconciliation behavior
retry rules
Without that, we would be inventing a fake financial integration.
Provider Investigation Output
The next provider investigation should produce:
supported refund operation
required original payment reference
full-refund rules
provider idempotency support
provider refund identifier
success response semantics
confirmed failure semantics
timeout/unknown semantics
status lookup/reconciliation capability
Once known, we can design the workflow confidently.
Engineering Principle
The first principle:
Impact analysis asks what the new requirement touches before asking how to implement it. A production change should alter only the responsibilities whose existing assumptions are no longer sufficient.
The second:
Existing boundaries are assets. Historical pricing, isolated provider integration, separate Payment workflow state, and explicit Order lifecycle reduce the blast radius of the refund change.
The third:
External financial side effects must be designed around uncertainty. A provider refund cannot participate in a PostgreSQL transaction, so durable attempt state, retry safety, and reconciliation must be understood before implementation.
And:
Knowing what must not change is part of safe system evolution. Refund support does not justify rewriting Product, Inventory, cancellation, Customer identity, or the entire architecture.
Summary
In this lesson, we assessed the refund requirement against the existing production system.
We established that:
- Order lifecycle must be revisited because
PAIDwas previously terminal only under the v1 no-refund scope. - We have not yet decided whether
REFUNDEDbelongs inOrderStatus. - Payment/refund processing states should not automatically become OrderStatus values.
- Existing
markPaid()semantics remain valid. - Refund must not be implemented through
cancel(). CancelOrderUseCaseremains an unpaid-order workflow.- Refund does not restore Inventory.
- Product state and current Product price must not participate in refund eligibility or amount.
- Historical Order pricing remains authoritative.
- Money representation in integer cents remains sufficient.
- The Payment external capability must gain refund support.
- Provider-specific refund HTTP behavior belongs in the provider adapter.
- Current provider refund semantics are a genuine implementation blocker.
- Existing
PaymentAttemptshould be reviewed but not blindly reused. - Refund likely needs its own durable external-operation state.
- The provider call must not occur inside a long database transaction.
- Refund needs multi-phase local/external/local coordination.
- Timeout must be treated as potentially unknown outcome.
- Blind refund retries may cause double financial effects.
- One unresolved refund operation per Order/payment is a likely invariant to protect.
- Refund idempotency must be designed together with provider capabilities.
orderspersistence may or may not change depending on Order lifecycle representation.- Some new persistence is definitely required.
- Historical paid Orders must remain eligible.
- Existing provider Payment references must be inspected for historical refundability.
- A new administrator command API is required.
- Customer-facing Order/read behavior must expose refund outcome somehow.
- API compatibility must be assessed before introducing a new enum value.
/api/v2should not be introduced automatically.- RFC 7807 mapping must expand for refund-specific business failures.
- Refund initiation requires administrator authority.
- Customer ownership does not imply refund authority.
- No new Customer aggregate is required.
- Structured logging must cover the refund workflow.
- Metrics may add bounded refund outcomes.
- Health checks should not automatically make refund-provider availability a global readiness dependency.
- Existing CI can execute the new tests.
- Refund requires new domain/application, provider, persistence, concurrency, recovery, authorization, and API tests.
- Migration and rollout compatibility are now explicit production concerns.
- The backlog will need to evolve after the design becomes clear.
- Product, Inventory, cancellation semantics, Create Order idempotency, Customer identity, money representation, and the modular-monolith architecture should remain stable.
Next lesson:
Investigating the Payment Provider Refund Contract
Before we design the final workflow, we need to resolve the real external unknowns:
How is a refund requested?
Which original payment reference is required?
Does the provider support idempotency?
Can the provider return an unknown outcome?
Can refund status be queried later?
What identifier represents the refund?
Can the same successful payment
be refunded more than once?
How do duplicate refund requests behave?
Once those answers are established, we can safely design:
RefundAttempt
provider boundary
transaction boundaries
reconciliation
concurrency
API behavior
without inventing financial semantics.
