Ten frames down
I needed to understand how a modular monolith worked. Nothing was broken. I just wanted to follow one request from the top.
I opened the controller and hit go to definition. Interface. The implementation called another service through another interface, which dispatched to a handler, which called a service, which called a repository interface with one repository behind it. I lost count somewhere past ten. Every one of them took the same argument, did nothing to it, and passed it on. At the bottom was a ternary, and it was the only line that decided anything.
So I ran it and stepped through, because stepping through was faster than reading. Somewhere around the sixth frame I started wondering whether this is what a modular monolith is meant to look like. Then whether it's what any code is meant to look like.

The metrics are fine with it
Run every tool you own over that stack and nothing comes back.
public Task<AccountStatus> GetStatus(Guid accountId, CancellationToken ct)
=> _repository.GetStatus(accountId, ct);
Cyclomatic complexity of one. There are no branches, so there is nothing to score. The nine above it are the same. Coupling is low, because each file knows about one interface. Every method is short. Coverage is whatever you want, because a mock returning Active and an assert that you got Active back will cover any of them.
The numbers are the science half and they're working correctly. They measure the shape of a file. Not one of them can tell you the file shouldn't be there.
You could build one that tries. Count the methods whose whole body hands the same arguments on, and that codebase would have lit up. So would the payment interface, which is right and should stay. A number finds candidates. It can't tell you which are load bearing, and the day it becomes a build gate somebody deletes the wrong one to get it green.
Ninety percent
Afterwards I took a copy and refactored it as an exercise, to see what was actually holding the thing up. About ninety percent came out, and what was left behaved the same.
Nearly all of what went was extension points. An interface, a registration, a seam at every layer, so the system was open for extension without anybody modifying what was already there. That's the open closed principle and it's a good one. What I had been reading was that principle applied everywhere, whether or not anything was ever going to extend.
Nothing ever came through ninety percent of those seams. Somebody was paid to write every one of them, and everybody since has been paid to read past them. That's burning money, and the metrics never noticed.
The trade
An interface means the call site can't see what's behind it. That's the property that lets you swap the implementation, and it's the same property that stops you following the code. You don't get one without the other.
So every seam is a trade. You give up reading straight through, and you get to change what's underneath without touching what sits above. Make that trade where something changes and you're ahead. Make it at every layer and you've paid the reading cost ten times over to collect once.
The principle already says to pick. Open for extension means open against the changes you decided to expect, not all of them. Most of the time the right answer is to use the object and let the call site say what it does. An abstraction is what you reach for when you think that will stop being true, and working out when is the job. Skip that and what's left is writing interfaces.
Payments is the easy version. You're on Mastercard, you can feel Visa coming, so you put an interface in front of the processor and nothing upstream ever learns which one it's talking to. Nobody computed that. Somebody guessed, and the guess came from having sat in the meetings where Visa kept coming up.

That's a judgement call about a future nobody can see, and the developer making it is the artist, whatever you think of the word.
All or nothing
Ask a model to add a status lookup and you get an interface, an implementation, a repository interface, a repository and an extension method to register the lot. Ask it for something else the same week and the query lands in the controller. I've had both out of the same model.
Neither one is a decision. It has read a very large number of codebases and gives back the shape they have. It was never in the meeting where somebody said Visa, and it doesn't know what your company is going to do next, which is the only input the call takes.
So the person driving it is the whole variable. Somebody who knows the second processor is coming can say where the seam goes and have it back in seconds, and that's the fastest pair of hands you will ever work with. Somebody who doesn't know takes the diff as it came, and the diff has ten layers in it, each of them scoring one.
The cost moved
A speculative layer is insurance. You pay now in case the change comes later and turns out to be expensive.
Both sides of that have moved. Writing the layer is nearly free now, and a model will write ten more on request. Reading them is the cost, and nobody counts it, because it doesn't land until somebody new has to understand the thing.
The claim side moved further. The expensive refactor it covers is the mechanical kind, pulling an interface through forty call sites, and that's what the tooling is good at now. The same model that can't make the call is fast at carrying one out.
A refactor is only cheap if something says what the behaviour was, though. In that codebase the tests were mocks returning Active and asserting Active came back, which pin nothing. That was the thing worth paying for, and it isn't where the money went.
To finish
Half of abstraction is measurable and the tools measure that half well. The other half is somebody's guess, and no tool has ever made one. Every layer built on a guess that didn't come off gets paid for twice, once to write it and every day since to read past it. That bill used to be unrecoverable. It isn't any more, which is the only part of this that's new.