I disagree with Uncle Bob's use of the term "use case" but I understand what he's getting at. I don't really want to quibble over the semantics of the term in any case.
For the sake of this question, use cases
are application specific business rules.
Your question is really "when are separate layers needed for both enterprise business rules and application specific business rules?" And the quick answer is you need them when your application grows large enough to justify it.
If there are a small number of rules from either set, then it's just as easy to keep the implementation of those rules within a single application layer. If there are lots of rules for both sets, then you'll want to break them out to specific layers.
Uncle Bob lays out a rule that the inner circles shouldn't know of the outer circles in his architectural diagram. And that's ultimately the answer to your question. As the rules evolve and have clear delineation from the others then you'll need to isolate them out to separate layers.
Front end <--> API Service -> Service -> Repository -> DB
Right. This's, essentially, the design by layers proposed by Spring Framework. So you are in the "Spring's right way".
Despite Repositories, these are frequently used as DAOs, the truth is that Spring developers took the notion of Repository from Eric Evans' DDD. Repository interfaces will look often very similar to DAOs because of the CRUD methods and because many developers strive to make repositories' interfaces so generics that, in the end, they have no difference with the EntityManager (the true DAO here)1. Repositories tho add other abstractions like queries or criteria to enhance the data access.
Translated into Spring components, your design is similar to
@RestController > @Service > @Repository > EntityManager
The Repository is already an abstraction in between services and data stores. When we extend Spring Data JPA repository interfaces, we are implementing this design implicitly. When we do this, we are paying a tax: a tight coupling with Spring's components. Additionally, we break LoD and YAGNI by inheriting several methods we might not need or wish not to have. Moreover, we are (implicitly) assuming that the persistence data model is also the domain data model. This is quite the endemic of Spring, after many years working with Spring frameworks, you end up making everything data-centric.
That said, extending Spring Data JPA repositories is not mandatory. We implement a more plain and custom hierarchy of classes.
@Repository
public class DBRepository implements MyRepository{
private EntityManager em;
@Autowire
public MyRepository (EntityManager em){
this.em = em;
}
//Interface implentation
//...
}
Changing the data source now just takes a new implementation which replaces the EntityManager with a different data source.
//@RestController > @Service > @Repository > RestTemplate
@Repository
public class WebRepository implements MyRepository{
private RestTemplate rt;
@Autowire
public WebRepository (RestTemplate rt){
this.rt = rt;
}
//Interface implentation
//...
}
//@RestController > @Service > @Repository > File
@Repository
public class FileRepository implements MyRepository{
private File file;
public FileRepository (File file){
this.file = file;
}
//Interface implentation
//...
}
//@RestController > @Service > @Repository > SoapWSClient
@Repository
public class WSRepository implements MyRepository{
private MyWebServiceClient wsClient;
@Autowire
public WSRepository (MyWebServiceClient wsClient){
this.wsClient = wsClient;
}
//Interface implentation
//...
}
and so on.2
Back to the question, I don't think you need more layers. The layer you propose is going to end up as a proxy in between services and repositories or as a pseudo-service-repository where to place code you are not sure whether it belongs to the business or to the persistence.
1: Unlike many developers think, repository interfaces can be totally different from each other because each repository serves different domain needs. In Spring Data JPA, the role DAO is played by the EntityManager. It manages the sessions, the access to the DataSource, mappings, etc.
2: A similar solution is enhancing Spring's repository interfaces mixing them up with custom interfaces. For more info, look for BaseRepositoryFactoryBean and @NoRepositoryBean. However, I have found this approach cumbersome and confusing.
Best Answer
While the “Clean Architecture” is fine and has many advantages, it is important to remember that:
The Clean Architecture is largely Robert C. Martin's re-branding and evolution of related approaches like the Onion Architecture by Jeffrey Palermo (2008) and the Hexagonal Architecture (“Ports and Adapters”) by Alistair Cockburn and others (< 2008).
Different problems have different requirements. The Clean Architecture and related approaches turn decoupling, flexibility, and dependency inversion up to eleven, but sacrifice simplicity. This is not always a good deal.
The precursor to these architectures is the classic MVC pattern from Smalltalk. This disentangles the model from the user interface (controller and view), so that the model does not depend on the UI. There are many variations of MVC like MVP, MVVM, ….
More complex systems do not have just one user interface, but possibly multiple user interfaces. Many apps choose to offer a REST API that can be consumed by any UI, such as a web app or a mobile app. This isolates the business logic on the server from these UIs, so the server doesn't care which kind of app accesses it.
Typically, the server still depends on backend services such as databases or third party providers. This is perfectly fine, and leads to a simple layered architecture.
The Hexagonal Architecture goes further and stops making a distinction between frontend and backend. Any external system might be an input (data source) or an output. Our core system defines the necessary interfaces (ports), and we create adapters for any external systems.
One classic approach for strong decoupling is a service oriented architecture (SOA), where all services publish events to and consume events from a shared message bus. A similar approach was later popularized by microservices.
All of these approaches have advantages, such as making it easier to test the system in isolation (by replacing all external systems it interfaces with by mock implementations). They make it easier to provide multiple implementations for one kind of service (e.g. adding a second payment processor), or to swap out the implementation of a service (e.g. moving from an Oracle database to PostgreSQL, or by rewriting a Python service in Go).
But these architectures are the Ferrari of architectures: expensive, and most people don't need them. The added flexibility of the Clean Architecture etc. comes at the cost of more complexity. Many applications and especially CRUD webapps do not benefit from that. It makes sense to isolate things that might change, e.g. by using templates to generate HTML. It makes less sense to isolate things that are unlikely to change, e.g. the backing database. What is likely to change depends on the context and business needs.
Frameworks make assumptions about what is going to change. E.g. React tends to assume that design and behaviour of a component change together, so it doesn't make sense to separate them. Few frameworks assume that you might want to change the framework. As such, frameworks do present an amount of lock-in. E.g. Rail's reliance on the (very opinionated!) Active Record pattern make it difficult to impossible to change your data access strategy to the (often superior) Repository pattern. If your expectations of change do not match the framework, using a different framework might be better. Many other web frameworks do not make any assumptions about data access.