Payments
How I Would Design a Payment System as a Junior .NET Developer
If I got handed a payment feature tomorrow, the hard parts wouldn't be the API design. They'd be idempotency, the fact that money problems don't show up in testing, and knowing what not to build.
If someone handed me a payment feature tomorrow, the API design would be the easy part. What I'd actually worry about is a shorter and less obvious list than I would have guessed a year ago.
I would not build the payment part
The first decision is what not to build. Card data has compliance obligations attached to it, and a junior developer deciding to handle raw card numbers to save an integration fee is making a decision that isn't theirs to make. Use a provider, let the card details go to them, and store the reference they give back.
That sounds like avoiding the problem. It's actually the correct answer, and knowing that is worth more than being able to implement the thing you shouldn't implement.
Assume every request arrives twice
This is the one I'd get wrong if I hadn't already learned it the hard way. Networks time out, users double-click, mobile clients retry automatically, and none of that is unusual. A payment endpoint that charges twice when it receives the same request twice is broken, even if it passes every test you wrote for it.
The fix is an idempotency key from the client and a unique constraint on it in the database. Checking whether the key exists before inserting looks like it works, but there's a window between the read and the write where two concurrent requests both pass the check. Let the database enforce it, catch the failure, and return the result you already stored.
Money should never be a float
decimal, always. Floating-point arithmetic loses precision in ways that are fine for a physics simulation and unacceptable for a balance. This one is easy to get right and genuinely bad to get wrong.
Store the currency alongside the amount too. An amount without a currency is a number, not money, and the moment a second currency shows up in the system, every amount without one becomes ambiguous.
Design for the question "what happened?"
The thing I underestimated most: a payment system's job isn't only to move money, it's to be able to explain afterward what it did and why. When a customer disputes a charge or a provider asks about a transaction, the answer has to come from stored records, not from reading the code and reasoning about what it probably did.
That means every meaningful step gets persisted, including the failures. It also means payment records don't get deleted, because a deleted payment record is destroyed evidence rather than tidied-up data.
Make states explicit and transitions guarded
A payment is a state machine whether or not you model it as one. It's pending, then it's authorised or declined, then maybe captured, then maybe refunded. If those states are just a string field that anything can write to, then eventually something writes an impossible combination and there's no record of how.
Putting the transitions inside the entity, so an invalid one throws instead of silently saving, costs very little and turns a class of bug into an exception you can actually see.
Failures are not all the same
Insufficient funds and a card reported stolen are both declines, and treating them the same way is a mistake. One is temporary and worth retrying, the other is final and retrying it elsewhere is just fraud with extra steps. Any retry logic needs to know which kind of failure it's looking at before it decides to try again.
What I would ask for help with
Reconciliation, the process of confirming that what your system thinks happened matches what the provider says happened. I understand it matters and I don't have real experience with it, and it's the kind of thing where being wrong is expensive and quiet.
Also anything involving multiple currencies with real conversion, and anything touching payouts rather than collections. I know enough to know those are deeper than they look.
The general shape of it
Use a provider for card data. Assume duplicate requests. Use decimal with an explicit currency. Persist enough to explain any transaction later. Model states explicitly. Distinguish retryable failures from final ones. And be honest about which parts you'd want a senior to review before it goes near real money.