Every payment system starts the same way: one table, one provider, ship it.
Then the second provider arrives. Then retry logic. Then partial refunds. Then you realize the model you built on day one is lying to you.
I went through this — and instead of patching it, I started over with one question: what is a payment as a business concept?
The result is a domain model built around DDD and hexagonal architecture: Payment as intent, PaymentAttempt as action, channels (Card, Crypto, P2P, Cash) each with their own state machine, and provider ports that keep the domain clean.
Full writeup with code, diagrams, and the reasoning behind every decision:
👉 https://corner4.dev/reinventing-payment-how-i-evolved-a-domain-model-from-one-table-to-ddd
The code is open source: https://github.com/payroad/payroad-core