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.
The Clean/Onion/Hexagonal architecture applies most clearly to microservices when viewing each microservice in isolation. Each MS has its own model and its own use cases and defines its own external interfaces/ports (both for supplying data and retrieving data). These interfaces can be implemented with an adapter that connects to another microservice, which may be as simple as providing an URL in the microservice's configuration. The internal architecture of a microservice is irrelevant to external users of that service.
With some contortions it is also possible to interpret a group of microservices through the Clean Architecture. Here the MSes together form the center model, whereas external gateways and routing infrastructure represent the outer layers. However, this viewpoint would be unusual and also breaks down when there is no clear separation between internal and external services: a service is usable as long as it can communicate over a network.
You mention that you will have several frontends. This can indeed mean that using an approach like the Clean/Onion/Hexagonal architecture is a good fit, because your business processes (the entities and models) will not be coupled to individual frontends. On the other hand, you should consider whether all these business processes form a common model, or whether they are separate bounded contexts in DDD-speak.
Your brief sketch of your requirements neither requires nor rules out microservices. Note that it can be an equally valid solution to put each service into a library and link those into a “monolithic” application. Microservices have clear benefits when
- you are willing to accept the difficulties of building a distributed system; and
- you either
- want to scale different parts of the system separately; or
- want to let separate teams develop and deploy their microservices independently.
Note that this is not an all-or-nothing approach, it can also make sense to build a mostly-monolithic application that just extracts individual components as separate services, for example a web app with a background job queue on a different server.
Difficulties of distributed systems not only include the administration of the system (administrating multiple servers, managing networks between them, possibly using Kubernetes) but also the tricky semantics for your application like distributed transactions (or the lack thereof), eventual consistency (aka. relaxing data consistency requirements), and overall system complexity from juggling parallel processes. Keeping processes stateless helps with the latter part. Sharing databases between microservices would make consistent and transactional data possible, but voids the separation benefits that microservices are typically used for.
Microservices have close to zero architectural advantages for taming complex systems. While MSes force you to keep separate components separate because there's a process level boundary between them, the same isolation is possible with libraries. However, the communication between microservices is effectively dynamically typed (unless you use a formal service description language that can generate the code for connectors). In contrast, static type checking between libraries can prevent some interface errors.
My opinion, given the little context available, is that a microservice-oriented solution is likely not a good fit for your organization. Your team may not have the experience to build distributed systems effectively, you likely do not need the technical scalability benefits, and with a single small team you definitively do not need the organizational benefits. In contrast, your interest at using the Clean Architecture as a guideline seems very sensible given your setting. It shines exactly for complex enterprise apps that need to integrate different systems. However, looking at domain driven design might also be very helpful to determine what should be part of the core model of your application, and when to spin off some aspects as a separate model/library/service.
Best Answer
If you're only holding data in your
Entities
your domain model will become anemic, because the encapsulation is broken and the behaviors are all defined outside the entity, far from where the data are declared.The whole point of the Clean Architecture (and the Hexagonal Architecture, Onion Architecture, etc.) is to promote the Dependency Inversion Principle, the D in the SOLID acronyme. To do that, the outer layers need to depend on the the inner layers, not the opposite. That's why it's not only ok but necessary that your interactors depend on your entities. But your entities should not depend on your interactors.
Testing interactors means testing a whole use case, so that's totally intended to execute the code in your entities !