S
Shaleen Mathur
Guest
Some architecture problems look fine while the system is small.
A few businesses. A few payment methods. Each business wants slightly different accounting, so you wire each payment method to each business.
Nothing looks broken yet.
Then both sides grow.
A new payment method shows up and suddenly needs accounting logic for every business already on the platform. A new business launches and every existing payment method has to learn it.
That is not a platform. That is a matrix.
I hit this on payment accounting. The lesson was not really about payments.
When two parts of a platform grow on their own and still know too much about each other, integration cost multiplies. Another abstraction layer or a fatter shared API is usually the wrong fix. You need a boundary where neither side has to know the other exists.
Picture B businesses and P payment methods.
If each payment method carries business-specific behavior, every intersection is an integration. Cost heads toward O(B × P).
Accounting makes this extra ugly because two flows that look the same to a customer can be very different in the books.
Pre-funded balance payments are the clean example.
The customer sees a balance, the balance drops, the order is paid.
Accounting sees a liability that has to come down. The business may need to recognize product revenue, tax, shipping, discounts, other pieces. Timing splits too. Some entries fire when payment is accepted. Others wait on fulfillment or delivery.
In the system I was in, those jobs had gotten tangled.
The same payment method could need different accounts, config, event reading, and posting logic depending on which business it served.
Two bad rules follow:
Add a business, payment teams have work. Add a payment method, business teams have work.
Neither side can move alone.
That is the smell I watch for now. If adding something on one side forces unrelated teams on the other side to change code, the boundary is in the wrong place.
The useful question was not how to map every pair. It was: why does the payment method need to understand the business?
It did not.
Payments need to account for the payment. The business needs to account for the sale. Related money events. Different jobs.
So we stopped letting the two domains talk directly and put a categorical intermediary in the middle. Call it a handoff.
The payment side settles against the handoff. The business side settles against the handoff. Neither settles against the other.
Small sentence. Large change.
On pre-funded balances, the payment system can debit pre-funded balance liability and credit the handoff. The business can debit the handoff and credit product revenue, tax, and shipping, on its own clock.
The handoff is the seam. The useful property is how little crosses it.
The shared contract does not need StoredValueBalanceForRetailOrderInCountryX. Amount: 100, Category: PRE_FUNDED_BALANCE is enough.
The moment the boundary starts carrying ideas from both domains, the matrix creeps back. A good handoff contract holds only what both sides actually need.
Once that seam exists, ownership gets boring in a good way.
The payment system owns payment behavior. When funds actually move. Whether the money came from an external account, an internal pre-funded balance, or something deferred. Its liabilities. Its lifecycle.
The business system owns business accounting. How an order splits into product revenue, taxes, shipping, discounts, fees, the rest. When those amounts become recognizable under its fulfillment model.
Neither side imports the other's domain model.
That was the big win.
People describe decoupling as fewer dependencies. I think that undersells it. The useful target is how little one team has to understand about the other team's world.
If a payment engineer has to learn every business's accounting before launching a method, the org does not scale, even if the services talk through APIs. If a business team has to know the guts of every payment instrument, those APIs are not protecting anyone.
Service boundaries do not automatically create team boundaries. You have to draw those on purpose.
There was a second mess hiding in the first. The platform had piled up dozens of payment types.
The obvious move is to expose all of them downstream. Businesses would be decoupled from implementations and still stuck with a long if/elif on payment_type: credit card, bank transfer, gift card, loyalty points, on and on. Keep that up and you rebuild the matrix one branch at a time.
The real question was not which instrument this is. It was: when does the customer's money actually move?
That one question collapsed dozens of instruments into three accounting behaviors.
For accounting integration, those behavioral differences mattered more than the brand on the instrument.
Downstream did not need dozens of methods. It needed three behaviors.
I keep finding this outside payments. When someone says the platform has 40, 60, or 100 types of a thing, I ask whether those are actually different behaviors. Usually they are not. One or two dimensions often explain the variation that matters. That is where the abstraction should sit.
Enumeration says: here is every thing the platform currently supports.
Classification says: here are the behavioral rules a thing has to satisfy.
Enumeration goes stale the moment you add something new. Classification stays useful because a new instrument just has to pick a behavior: Immediate Settlement, Pre-funded Balance, or Deferred Settlement. The handoff contract does not grow. The matrix does not come back.
Once the handoff exists, something still has to move events through it. Keep that thing thin.
A smart orchestrator wants to know both sides. It starts interpreting payment lifecycle on behalf of the business, or splitting revenue on behalf of payments. That knowledge feels helpful. It is how the matrix sneaks back in through a third service.
A thin orchestrator only routes. Payment posted against the handoff. Business settled against the handoff. Amount, category, maybe a correlation id. If the orchestrator needs StoredValueForRetailOrderInCountryX to do its job, the seam is already leaking.
Put the cleverness where the ownership already lives. Payments decide when money moved. The business decides when revenue is recognizable. The middle should be boring.
The test is not whether the current matrix works. The test is what happens when you add one more thing.
Add a payment method. If business teams have to change posting logic, accounts, or if/elif chains, you are still at O(B × P). If they keep settling against the same handoff, and the new method only has to pick Immediate Settlement, Pre-funded Balance, or Deferred Settlement, you are closer to O(B + P).
Add a business. If payment teams have to learn that business's tax, shipping, and fulfillment timing, the boundary failed. If the business team maps the handoff into its own books and payments never hear about it, the boundary held.
One new method, zero business-code changes. One new business, zero payment-code changes. That is the scoreboard. APIs that still force both sides to ship together are not a platform yet.
Coupling has an organizational cost
The matrix is not only an engineering tax. It is a staffing tax.
Payment people waiting on business people. Business people waiting on payment people. A launch calendar that cannot move until two orgs understand each other's internals. That is the same O(B × P), expressed as meetings.
Reducing how much one team has to know about the other team's world is the point. If a payment engineer still needs every business's accounting before a method can go live, the org will not scale even if the services already talk through APIs. If a business team still needs the guts of every instrument, those APIs are theater.
Service boundaries do not automatically create team boundaries. Draw those on purpose, then check them with the add-one-more test.
Payments made this obvious. The shape is not unique to payments.
Whenever two independently growing catalogs sit behind a grid of custom integrations, you get the same smell. Add an item on one axis and the other axis has work. Forty, sixty, a hundred "types" that are really two or three behaviors. Downstream if/elif that recreates the matrix a branch at a time.
Ask the same questions. What is the smallest contract both sides need? Who owns which complexity? What happens when you add one more? If the answers still name both domains, the seam is in the wrong place.
A platform that is working does not make the next payment method a special project. It makes it a classification. Pick the behavior. Settle against the handoff. Ship.
The next business should feel the same. Map the handoff into product revenue, tax, shipping, discounts, fees, whatever that business actually recognizes. Payments do not attend.
If every new integration is still a custom pairing, you are maintaining a matrix and calling it a platform. The interesting work should stay inside each domain. Crossing the seam should be dull.
That is the move from O(B × P) to O(B + P). Not a fatter shared API. A boundary so small that neither side has to know the other exists, and adding one more thing on either side stays a local change.
A few businesses. A few payment methods. Each business wants slightly different accounting, so you wire each payment method to each business.
Nothing looks broken yet.
Then both sides grow.
A new payment method shows up and suddenly needs accounting logic for every business already on the platform. A new business launches and every existing payment method has to learn it.
That is not a platform. That is a matrix.
I hit this on payment accounting. The lesson was not really about payments.
When two parts of a platform grow on their own and still know too much about each other, integration cost multiplies. Another abstraction layer or a fatter shared API is usually the wrong fix. You need a boundary where neither side has to know the other exists.
When system A knows which system B it is talking to, you have a matrix
Picture B businesses and P payment methods.
If each payment method carries business-specific behavior, every intersection is an integration. Cost heads toward O(B × P).
Accounting makes this extra ugly because two flows that look the same to a customer can be very different in the books.
Pre-funded balance payments are the clean example.
The customer sees a balance, the balance drops, the order is paid.
Accounting sees a liability that has to come down. The business may need to recognize product revenue, tax, shipping, discounts, other pieces. Timing splits too. Some entries fire when payment is accepted. Others wait on fulfillment or delivery.
In the system I was in, those jobs had gotten tangled.
The same payment method could need different accounts, config, event reading, and posting logic depending on which business it served.
Two bad rules follow:
Add a business, payment teams have work. Add a payment method, business teams have work.
Neither side can move alone.
That is the smell I watch for now. If adding something on one side forces unrelated teams on the other side to change code, the boundary is in the wrong place.
The better pattern is a Handoff
The useful question was not how to map every pair. It was: why does the payment method need to understand the business?
It did not.
Payments need to account for the payment. The business needs to account for the sale. Related money events. Different jobs.
So we stopped letting the two domains talk directly and put a categorical intermediary in the middle. Call it a handoff.
The payment side settles against the handoff. The business side settles against the handoff. Neither settles against the other.
Small sentence. Large change.
On pre-funded balances, the payment system can debit pre-funded balance liability and credit the handoff. The business can debit the handoff and credit product revenue, tax, and shipping, on its own clock.
The handoff is the seam. The useful property is how little crosses it.
The shared contract does not need StoredValueBalanceForRetailOrderInCountryX. Amount: 100, Category: PRE_FUNDED_BALANCE is enough.
The moment the boundary starts carrying ideas from both domains, the matrix creeps back. A good handoff contract holds only what both sides actually need.
Let each side own its own mess
Once that seam exists, ownership gets boring in a good way.
The payment system owns payment behavior. When funds actually move. Whether the money came from an external account, an internal pre-funded balance, or something deferred. Its liabilities. Its lifecycle.
The business system owns business accounting. How an order splits into product revenue, taxes, shipping, discounts, fees, the rest. When those amounts become recognizable under its fulfillment model.
Neither side imports the other's domain model.
That was the big win.
People describe decoupling as fewer dependencies. I think that undersells it. The useful target is how little one team has to understand about the other team's world.
If a payment engineer has to learn every business's accounting before launching a method, the org does not scale, even if the services talk through APIs. If a business team has to know the guts of every payment instrument, those APIs are not protecting anyone.
Service boundaries do not automatically create team boundaries. You have to draw those on purpose.
Dozens of types usually do not mean dozens of behaviors
There was a second mess hiding in the first. The platform had piled up dozens of payment types.
The obvious move is to expose all of them downstream. Businesses would be decoupled from implementations and still stuck with a long if/elif on payment_type: credit card, bank transfer, gift card, loyalty points, on and on. Keep that up and you rebuild the matrix one branch at a time.
The real question was not which instrument this is. It was: when does the customer's money actually move?
That one question collapsed dozens of instruments into three accounting behaviors.
- Immediate Settlement: money is collected from an outside source when the transaction happens. Cards, bank methods.
- Pre-funded Balance: the customer already funded a balance inside the platform. Gift cards, loyalty, credits, that family.
- Deferred Settlement: the customer gets the goods or service before the cash is real. Invoices, installments.
For accounting integration, those behavioral differences mattered more than the brand on the instrument.
Downstream did not need dozens of methods. It needed three behaviors.
I keep finding this outside payments. When someone says the platform has 40, 60, or 100 types of a thing, I ask whether those are actually different behaviors. Usually they are not. One or two dimensions often explain the variation that matters. That is where the abstraction should sit.
Classification beats enumeration
Enumeration says: here is every thing the platform currently supports.
Classification says: here are the behavioral rules a thing has to satisfy.
Enumeration goes stale the moment you add something new. Classification stays useful because a new instrument just has to pick a behavior: Immediate Settlement, Pre-funded Balance, or Deferred Settlement. The handoff contract does not grow. The matrix does not come back.
Thin orchestration is better than smart orchestration
Once the handoff exists, something still has to move events through it. Keep that thing thin.
A smart orchestrator wants to know both sides. It starts interpreting payment lifecycle on behalf of the business, or splitting revenue on behalf of payments. That knowledge feels helpful. It is how the matrix sneaks back in through a third service.
A thin orchestrator only routes. Payment posted against the handoff. Business settled against the handoff. Amount, category, maybe a correlation id. If the orchestrator needs StoredValueForRetailOrderInCountryX to do its job, the seam is already leaking.
Put the cleverness where the ownership already lives. Payments decide when money moved. The business decides when revenue is recognizable. The middle should be boring.
The best test of decoupling: add one more
The test is not whether the current matrix works. The test is what happens when you add one more thing.
Add a payment method. If business teams have to change posting logic, accounts, or if/elif chains, you are still at O(B × P). If they keep settling against the same handoff, and the new method only has to pick Immediate Settlement, Pre-funded Balance, or Deferred Settlement, you are closer to O(B + P).
Add a business. If payment teams have to learn that business's tax, shipping, and fulfillment timing, the boundary failed. If the business team maps the handoff into its own books and payments never hear about it, the boundary held.
One new method, zero business-code changes. One new business, zero payment-code changes. That is the scoreboard. APIs that still force both sides to ship together are not a platform yet.
Coupling has an organizational cost
The matrix is not only an engineering tax. It is a staffing tax.
Payment people waiting on business people. Business people waiting on payment people. A launch calendar that cannot move until two orgs understand each other's internals. That is the same O(B × P), expressed as meetings.
Reducing how much one team has to know about the other team's world is the point. If a payment engineer still needs every business's accounting before a method can go live, the org will not scale even if the services already talk through APIs. If a business team still needs the guts of every instrument, those APIs are theater.
Service boundaries do not automatically create team boundaries. Draw those on purpose, then check them with the add-one-more test.
The same pattern shows up everywhere
Payments made this obvious. The shape is not unique to payments.
Whenever two independently growing catalogs sit behind a grid of custom integrations, you get the same smell. Add an item on one axis and the other axis has work. Forty, sixty, a hundred "types" that are really two or three behaviors. Downstream if/elif that recreates the matrix a branch at a time.
Ask the same questions. What is the smallest contract both sides need? Who owns which complexity? What happens when you add one more? If the answers still name both domains, the seam is in the wrong place.
A platform should make the next integration boring
A platform that is working does not make the next payment method a special project. It makes it a classification. Pick the behavior. Settle against the handoff. Ship.
The next business should feel the same. Map the handoff into product revenue, tax, shipping, discounts, fees, whatever that business actually recognizes. Payments do not attend.
If every new integration is still a custom pairing, you are maintaining a matrix and calling it a platform. The interesting work should stay inside each domain. Crossing the seam should be dull.
That is the move from O(B × P) to O(B + P). Not a fatter shared API. A boundary so small that neither side has to know the other exists, and adding one more thing on either side stays a local change.