Interfaces are everywhere, but still we have tight coupling, and inflexibility. Dependency inversion is the answer.
It feels like interfaces are the vogue thing in programming today, almost an automatic reflex for some, and it tends towards systems where there are a million interfaces with single implementations that aren't very useful.
There is one obvious consideration here - unit tests. Unit tests almost require everything to be an interface, because an interface can be simulated. When you can simulate all the parts that a class needs, you can test that class in isolation. So, for this reason alone, an interface can be pretty useful.
But taking that out of the picture - if the need to unit test was not a consideration - do the interfaces actually offer any useful utility?
There is a further, more powerful utility in using interfaces that is for the most part, underappreciated in programming - dependency inversion.
Most programmers think they're creating dependency inverted code, with loose coupling when they require an interface instead of the implementation - but this is not automatically true.
Dependency inversion means that the flow of dependency is against the flow of control. This means, the business logic that has to read from or save to the database should not depend on the database - the database should depend on it!
This is something that almost never happens. Instead, we see interfaces to the database are passed into the business logic, and implemented by a DBContext class of some kind, or stored procedure calls or similar. But the interfaces are still organised around the structure of what is convenient to the database.
This means that the business logic layer has to do the work of mapping the database objects into something that is useful to itself. If it wants to save something that effects multiple tables, it has to call multiple saves to those multiple tables - so it's far from being independent from the database - if the database structure were to change, the business logic (and all it's unit tests that simulate the database) would also need to change.
So even though the business logic layer may not literally call the implementation of the database layer - is it actually effectively any less coupled to it? Definitely not in any meaningful way - it's still coupled by the data contracts (i.e. methods and class parameters) of the interface. We can simulate the database but we can't easily change it for something else.
A business layer that depends on a database interface is just as tightly coupled as a business layer that depends on a database implementation.
You could not, for example, easily replace the database with disk access without changing the business layer, unless you were willing to make the disc format closely resemble the database, and/or have other mapping code in place that makes the disk look like a database.
So how do we do true dependency inversion?
It's very simple - define the interfaces on the other side of the flow of control. But don't just move the existing interface - write the interface in terms of what the business logic wants to do - make the interface make sense to belong on that other side of flow. For instance, instead of having a database interface for the use of the business layer, make a business layer interface that the database has to fulfil. Make it a business-logic-first interface. Then, the database has to do the heavy work of mapping what the business logic wants to a database request (or multiple requests).
This does mean you probably will have different entity classes - this is a good thing! Don't reuse the database entity classes for the business logic if they're not a good fit - it just makes the business logic cumbersome anyway.
This even makes your unit tests easier to write - you don't have to simulate what a database is doing anymore, and you don't have to think about dummy data for a bunch of irrelevant things that happen to be present in the entities but aren't relevant in the tested situation - you just provide the end outcome that's actually relevant to the logic you're testing.
But better still - dependency inversion facilitates cleaner code and gives you a really clear description about what the business logic needs to get it's job done. And, you could create new implementations that the busines could use without changing it at all, and you can make those implementations deployment specific.
For instance, later down the line your web server code may want to offer a fat client and you may want to use disk storage instead of database storage, or you may want to develop a mobile app and you want to call the business logic to validate requests before going to some kind of API, but you don't want the app to actually save anything (must go to API first to do that).
Even if this never happens, it still makes the business logic code nice and clean, because it really doesn't care about the database, so all the code is just about what needs to be done, not how it's done.
This is applicable not just between the business logic layer and database - it's also true for many other layer-to-layer communications, but it's most often the buisiness logic to database layer that has dependency in the wrong direction. For instance, the web-site/presentation layer is usually dependent on the business logic. Imagine if it was the other way around! Business logic starts outputting HTML and CSS, and then you face the challenge of implementing a mobile app, or an API endpoint instead of a website, or even just wanted to do a redesign? Suddenly it's a massive headache.
Reorganising the database is usually a massive headache - but it could be less so, if the interfaces were defined with dependency in the right direction - against the flow of control.