Layered Modular Architecture: Evolving Clean Architecture for Multiplatform and Reduced Duplication

Layered Modular Architecture Overview
This article introduces the approach to the Layered Modular Architecture(LMA), demonstrated through the Sunflower Clone project. Note: Edited in 2025/1/11.

Disclaimer:
This is not an entirely new architecture but rather an extension of Clean Architecture, introducing new concerns and a revised modularization strategy. Its relationship to Clean Architecture is similar to that of MVI to MVVM. Given these differences, I felt it deserved its own name, even though it shares the same foundational concepts.
Introduction
How can Clean Architecture be adapted to meet modern demands for multiplatform support?
Clean Architecture is a popular choice for Android applications, as it provides a structure that is easier to maintain and scale. However, implementing multiplatform support is not straightforward because neither modularization by feature nor modularization by layer effectively separates platform-independent logic into modules. A more flexible modularization strategy is needed to enhance code reusability.
Architectural Design Overview
Layered Architecture
To address the challenges introduced by multiplatform considerations, LMA employs a five-layer approach that extends the typical data-domain-presentation structure used in implementing Clean Architecture:
- Core Layer:
A newly introduced layer responsible for hosting infrastructure that is independent of the domain model. - Domain Layer:
The responsibilities of the domain layer remain the same as in Clean Architecture. It contains the domain model, represented by entities, repository interfaces, and use cases. - Data Layer:
This layer contains concrete implementations of the repository interfaces declared in the domain layer. It integrates data sources to provide access to domain entities. - Feature Layer:
This layer is responsible for hosting functionalities that center around user interactions and visual representations. - App Layer:
A new layer serves as the entry point for applications, handling top-level concerns such as dependency injection, feature management, and other configurations.
With the introduction of the app layer, responsibilities for dependency injection and cross-feature interactions have been lifted from the presentation layer. The feature layer replaces the traditional presentation layer, placing greater emphasis on isolated functionalities.
Modularization in Layers
![]() |
Modularization in Sunflower Clone |
We cannot simply modularize the project using the previously mentioned five layers because addressing platform-dependent concerns for different platforms within a single module is impractical. To accommodate this, we further divide each layer based on layer-specific features:
- A layer-specific feature refers to the division of functionality within a specific layer, based on the distinct domains of that layer. This concept draws inspiration from the Domain-Driven Design (DDD) approach, applied individually to each layer.
- For example, if the domain layer contains three entities —
PlantDO
,PhotoDO
, andGardenPlantingDO
— we divide the data layer into separate modules based on these entities::data:plant
,:data:photo
, and:data:garden_planting
. Each module provides the necessary data access specific to its entity. - If
PlantDO
needs to be accessible on both Android and iOS platforms, we can further divide:data:plant
into two distinct modules, each providing implementation specific to its platform. - It is important to recognize that each layer is responsible for different concerns, and dividing modules solely based on business logic results in duplication.
- This modularization strategy draws inspiration from Google’s modularization recommendations, particularly from their YouTube video: By Layer or Feature? Why Not Both?! Guide to Android App Modularization.
This strategy enables platform-specific and platform-independent components to coexist within the same layer, while maintaining the separation of concerns outlined by the layered architecture.
Multiple Applications Structure
![]() |
Application Variants in Sunflower Clone |
With the app layer and the modularization strategy, we can create application variants to address different concerns:
- For example, in the Sunflower Clone project, I created multiple app modules to demonstrate the use of different navigation frameworks. Specifically, these modules are
:app:sunflower_clone
,:app:sunflower_clone_compose
, and:app:sunflower_clone_navigation
. - Each of these app modules represents a distinct approach to navigation within the same overall project, serving as layer-specific features within the app layer.
- The way to split layer-specific features is by determining the purpose of the application. If you need a demo version of an application, there is a layer-specific feature in the app layer for the demo.
By separating top-level concerns from the rest of the project, we can tailor each application to its specific purposes while leveraging shared implementations from other layers.
Internal Modules Within a Layer
![]() |
Internal Modules in Sunflower Clone |
Sometimes, two layer-specific features may contain duplicated logic that can be extracted into a shared module:
- An internal module within a layer provides a space to share logic among the modules within the same layer.
- For example, in the Sunflower Clone project, the module
:app:internal:sunflower_clone_config
shares metadata and dependency injection (DI) modules across:app:sunflower_clone
,:app:sunflower_clone_compose
, and:app:sunflower_clone_navigation
. - However, internal modules should not be referenced by other layers to prevent leakage of internal logic, as such modules do not qualify as layer-specific features.
- Whenever possible, shared code should be placed in modules from another layer that can be accessed by all relevant modules. This approach promotes the separation of concerns between layers.
- Alternatively, if the shared code can be directly referenced by another regular module within the same layer, it is preferable to introducing an internal module. This approach enhances reusability across layers, as a regular module can be utilized by other layers.
- For instance, the module
:feature:navigation
can reference:feature:plant_list
to make the plant list screen one of its tab pages. In this scenario,:feature:plant_list
remains a standalone feature that can still be independently integration-tested by an app module.
While LMA promotes reusability through a strict separation of concerns, modules within the same layer do not guarantee complete isolation. In cases where logic duplication cannot be avoided, using an internal module should be considered a last resort, as other approaches offer more benefits.
Dependency Inversion Principle
![]() |
Dependency Inversion in Layered Modular Architecture |
The modularization strategy of LMA does not guarantee a one-to-one relationship between the modules of the feature layer and the modules of the data layer. It is possible for two application variants to differ only in their data implementations:
- The Dependency Inversion Principle (DIP) states that high-level modules should not depend directly on low-level modules. Instead, both should depend on abstractions.
- In the context of LMA, the domain layer acts as the abstraction between the high-level feature modules and the low-level data modules. The app layer is responsible for selecting and injecting the required data implementations.
- For example,
:feature:plant_list
does not depend on:data:plant
. Instead, both depend on:domain
. The:app:sunflower_clone
module references:data:plant
and injectsPlantRepositoryImp
as thePlantRepository
instance used by:feature:plant_list
.
This adherence to DIP enables an app module to swap its data implementations without altering the feature modules. As a result, feature modules remain independent and reusable, with a narrowed scope focused solely on their functionality. Combined with the multiple app structure, this approach enables multiplatform support by allowing platform-specific applications to share platform-independent modules in the core, data, and domain layers.
Coordinator Pattern
![]() |
App Module Coordinating the Feature Modules in Sunflower Clone |
The Dependency Inversion Principle (DIP) has successfully decoupled the feature layer from the data layer. However, coupling still exists within the feature modules themselves, particularly due to the need to reference multiple feature modules for navigation:
- The Coordinator Pattern is used to decouple destinations from the navigation flow they belong to. It suggests that a host should manage the navigation, rather than allowing each destination to handle its own navigation.
- To address the issue mentioned earlier, the coupling logic between features must be isolated. In the context of LMA, the app layer serves as the coordinator for feature modules, handling navigation concerns.
- For example,
NavigationFragment
from:feature:navigation
does not need to directly referencePlantDetailsFragment
from:feature:plant_details
to navigate between them. Instead,MainActivity
from:app:sunflower_clone
handles these operations.
By adopting the Coordinator Pattern, feature modules remain independent unless a feature module is explicitly part of another. This results in isolated feature modules that are small, easy to maintain, and highly reusable.
External Infrastructure
![]() |
Dependencies Referencing the Core Layer in Sunflower Clone |
Applications often require communication with servers to fetch data or utilize persistence mechanisms to store information. These protocols and tools are technical concerns and do not depend on the domain layer:
- Non-business-specific infrastructure are placed in the core layer to further enhance the separation of concerns.
Examples from the Sunflower Clone project:
:core:unsplash_api
contains API protocols and is referenced by:data:photo
to fetch photos of plants.:core:sunflower_clone_database
implements the local SQLite database and is utilized by:data:garden_planting
,:data:plant
, and:data:photo
to persist domain entities.:core:sunflower_clone_ui
includes a shared UI kit and is used by the feature modules to standardize visual presentations.
By introducing the core layer, modules within other layers become further decoupled from one another. This decoupling makes modules more isolated, reusable, and maintainable, ensuring that infrastructure is shared without compromising modularity.
Implementation Details
![]() |
Dependency Graph of Sunflower Clone |
Each layer of LMA is a group of modules and the modules are divided by layer-specific features. We will refer to any of the modules by using the layer name as a prefix. For example, a module in the core layer is called a core module.
Core Layer
Core Layer in Sunflower Clone |
The core layer is where you place non-business-related infrastructure and share it with the other layers.
Concerns:
- API Client:
The protocols used to communicate with external servers are considered separate from the business contracts of the project. These are typically shared across multiple data modules to implement data access for domain entities. - Database:
While a database is necessary to persist domain objects, its schema is not inherently tied to business contracts. Instead, it primarily focuses on I/O operations. Databases are also usually shared across multiple data modules to implement data access for domain entities. - UI Kit:
The visual design of an application is not directly tied to business contracts and can be reused across different applications. UI kits are typically shared across feature modules and app modules to provide reusable UI widgets. - Other Infrastructures:
Other implementations that can be decoupled from the domain model may also belong in the core layer. For example, a module that provides encryption functionality may not be directly tied to business concerns and can be placed in the core layer for shared use.
Allowed Dependencies:
- Core Modules:
Core modules can freely depend on other core modules, as they remain independent of the domain model.
Disallowed Dependencies:
- Domain Modules:
The core layer should not interact with business logic, as this violates the separation of concerns. - Data Modules:
The data layer depends on the domain layer. Making the core layer depend on the data layer breaks the separation of concerns. - Feature Modules:
The feature layer also depends on the domain layer. Allowing the core layer to depend on the feature layer breaks the separation of concerns. - App Modules:
The app layer is the topmost layer and inherently depends on the domain layer. Allowing the core layer to depend on the app layer breaks the separation of concerns.
Platform Dependency:
- A core module can be either platform-dependent or platform-independent, depending on whether the infrastructure is platform-dependent.
Domain Layer
![]() |
Domain Module in Sunflower Clone |
The domain layer hosts the domain model, which the app, data, and feature layers depend on. The domain model serves as a bridge to the features and data, decoupling their concerns.
Concerns:
- Entities:
Entities are data objects that represent the core concepts of the domain. For example,PlantDO
is an entity in the Sunflower Clone project. - Repository Interfaces:
A repository is responsible for abstracting data access to its entity from the rest of the project.PlantRepository
is a good example of a repository interface. - Use Cases:
A use case encapsulates a single task that is part of the business logic. You don’t need to create use cases for every task if a repository alone is sufficient. A use case is usually preferred when the task involves multiple repositories or is complicated enough to be reused. - Additional Data Types:
Sometimes, entities, repository interfaces, and use cases alone are not enough to represent your domain model. For instance, a business-specific exception may be required to represent a unique failure in the domain model.
Allowed Dependencies:
- Domain Modules:
Domain models can be merged by allowing domain modules to depend on one another.
Disallowed Dependencies:
- Core Modules:
The domain layer shouldn’t depend on a platform-dependent module to maintain the separation of concerns. - Data Modules:
The data layer depends on the domain layer to reference entities and repository interfaces. The reverse is not allowed. - Feature Modules:
The feature layer depends on the domain layer to reference entities, repository interfaces, and use cases. The reverse is not allowed. - App Modules:
The app layer is the topmost layer and references both the data layer and the feature layer. Allowing the domain layer to depend on the app layer would create circular dependencies.
Platform Dependency:
- A domain module should be isolated to keep your business logic separate from other concerns. Therefore, it should not be platform-dependent.
The data layer contains the concrete implementations of repository interfaces. It is responsible for interacting with data sources and providing access to domain entities.
Concerns:
- Entity Mappings:
The data layer contains the knowledge required to map DTOs (from servers or databases) to domain entities. - Repository Implementations:
A repository implementation fulfills the contract defined by a repository interface. It handles interactions with data sources to retrieve, update, or store data.
Allowed Dependencies:
- Domain Modules:
The data layer must reference the domain layer to implement repository interfaces. - Core Modules:
The data layer often requires access to external infrastructure, such as API clients or databases, provided by the core layer.
Disallowed Dependencies:
- Data Modules:
A data module should not depend on the implementation of another data module. Cross-repository operations can be handled using use cases in the domain layer. - Feature Modules:
The data layer should not depend on feature-specific logic to maintain separation of concerns. - App Modules:
The data layer must remain independent of application-specific configurations to preserve reusability.
Platform Dependency:
- A data module can be either platform-dependent or platform-independent, depending on whether any of its data sources are platform-dependent.
Feature Layer
Feature Layer in Sunflower Clone |
The feature layer contains isolated functionalities of the applications. Each feature module centers around a platform-specific component, such as a fragment or service, which utilizes the domain layer for implementation.
Concerns:
- Controllers:
Define screen elements, such as composable functions or fragments, to receive user interactions and display content. A controller does not necessarily represent a screen and can also be part of other screens. - View Models:
A view model manages the state from its controller and provides state transformation upon user interactions, separating UI from the underlying logic. - Layouts:
Define how the UI is structured, such as using XML layouts or composable functions to control the visual representation of controllers. - Other Platform Components:
An isolated functionality does not necessarily mean a screen; sometimes, it can be something like a service for background tasks.
Allowed Dependencies:
- Domain Modules:
The feature layer relies on business contracts to provide functionality based on user interactions. - Core Modules:
The feature layer can reference the core layer to access external infrastructure, such as a UI kit for theming. - Feature Modules:
A feature module may depend on other feature modules for nested scenarios. For example, a fragment can contain a nested fragment, where both are features. This kind of coupling should be kept to a minimum.
Disallowed Dependencies:
- Data Modules:
The feature layer does not rely on concrete data implementations; instead, it depends on the domain layer for business contracts. - App Modules:
The feature layer should remain independent of app-specific configurations, as these are meant to be reused across applications.
Platform Dependency:
- A feature module is platform-dependent because it represents a functionality of the application. To provide this functionality, it requires a platform-specific component to host interactions with the domain model.
App Layer
App Layer in Sunflower Clone |
The app layer assembles the modules from other layers into complete applications. An app module serves as the feature coordinator and provides real instances of data implementations.
Concerns:
- Dependency Injection:
An app module typically references a set of feature modules that access data through repository interfaces. The repository implementations required by the feature modules must be injected into the app module. Sometimes, an app module may also need to inject external infrastructure, such as an API client or database, from the core layer. - Navigation:
An app module is responsible for managing the navigation flow of features in the application. - Top-level Features:
An app module includes the top-level feature to host other features and manage their interactions. This feature can contain its own controller, view model, and layout. Sometimes, an app module doesn’t need to reference feature modules and relies solely on the top-level feature to function as the application itself. - Other Application Configs:
An application may require additional configurations at the application scope. For example, metadata required by an application should also be handled inside the app module.
Allowed Dependencies:
- Domain Modules:
The app layer relies on the domain layer to communicate with features and data. - Data Modules:
The app layer provides the concrete data implementations required by the feature layer. - Feature Modules:
The app layer is responsible for integrating feature modules into the applications. - Core Modules:
The app layer may need to inject common infrastructure for the data layer, such as an API client or database.
Disallowed Dependencies:
- App Modules:
An app module is the entry point of an application and shouldn’t be depended on by another application. Allowing dependencies between them could lead to circular dependencies.
Platform Dependency:
- An app module is platform-dependent because it is the entry point of the application.
Platform Dependency:
- An app module is platform-dependent because it is the entry point of the application.
If a project adopts LMA as its architecture, these concrete rules alone should be enough for any experienced developer to work on it. LMA isn’t just an architecture designed to meet multiplatform requirements but also to provide a better development experience and increased efficiency. Next, we will discuss other benefits of LMA.
Pros, Cons, and Discussion
Pros of LMA
- Reduced Cognitive Load:
The foundational building block of LMA is a module, each representing a layer-specific feature. Each module focuses on a narrower scope compared to modularization by feature or layer, simplifying onboarding. - Inherent Scalability:
Unless you are modifying the domain layer or the core layer, adding new feature modules does not affect the stability of the project. The layered structure remains consistent regardless of the number of features. - Parallel Development of Feature and Data:
The server API is a common bottleneck in application development. Application developers often have to wait for backend developers to complete their implementations before they can resume progress. LMA separates the concrete data implementation from feature modules, as the domain layer serves as the contract, allowing features to be developed before the data implementation. - Framework Flexibility:
LMA does not bind to specific frameworks. The Sunflower Clone project demonstrated the use of Hilt for dependency injection, but it is also possible to use Koin or even manual injection instead. Different UI frameworks, such as XML layouts and Compose, can be used for different feature modules within the same project without conflicts. - Reduced Incremental Build Time:
The ripple effect of a single edit to the code is greatly reduced thanks to the smaller building blocks of LMA. Handling dependency injection in a single module for each application also reduces build time, as class path aggregation in a multi-module application can be time-consuming.
Cons of LMA
- Steep Learning Curve:
LMA builds upon the foundation of Clean Architecture, making the prerequisite knowledge harder to acquire. The combination of various software patterns, such as the dependency inversion principle and coordinator pattern, makes LMA a complex concept. However, the rules of LMA are solid and easy to follow, even for someone who doesn’t understand the theories behind it. - Discipline to Maintain Dependency Rules:
The flexibility of LMA can be a double-edged sword. There are no specific classes or frameworks necessary to implement LMA, and nothing enforces the separation of dependencies. It is easy for an unfamiliar developer to violate the project structure by establishing a prohibited reference in the code. - Runtime Exception from Missing Dependencies:
If an application utilizes lazy loading for dependency injection, it is possible to encounter a runtime exception when a required dependency is missing. This can be a debugging nightmare because such errors won’t be caught by the compiler. - Overhead for Small Projects:
Sometimes an application is too small to justify the need for separation of concerns. For example, applications with only one feature have no reason to integrate a feature layer. However, it is rare for any application to remain at the same scale nowadays. If an application fails to establish an architecture that is inherently scalable, it will require an overhaul or refactor to expand functionality in the near future. Additionally, you can always create more modules when necessary, as long as the overall architecture remains consistent.
Conclusion
Throughout my career as a software developer, particularly in startup environments, I’ve faced numerous situations where I had to handle entire enterprise projects on my own. These scenarios demanded agility and the ability to quickly adapt to new requirements.
Layered Modular Architecture (LMA) has proven invaluable in managing such projects. It allows for isolated changes across an entire system, ensuring that updates don’t interfere with other parts of the application. The architecture’s scalability helps prevent complexity from growing exponentially as new features are added, enabling teams to keep pace with the rapidly evolving business landscape.
When compared to the official Now in Android app tutorial, which demonstrates the recommended architecture, LMA offers a flatter dependency graph, courtesy of the Dependency Inversion Principle. This makes it significantly easier to navigate and locate code related to specific concerns.
In essence, LMA presents a more efficient, scalable alternative for new projects, providing a robust foundation for multi-platform development while simplifying ongoing maintenance and adaptation to changing needs.
留言
張貼留言