Konsist is a brand-new tool that helps with codebase standardization. It is compatible with various Kotlin projects including Android projects, Spring projects, and Kotlin Multiplatform projects.
Before diving into Konsist let’s briefly discuss the layered architecture. Components within the layered architecture pattern are organised into layers, each layer performing a specific role within the application. Each layer also defines the set of boundaries — classes and interfaces are only accessed by other classes and interfaces from desired layers.
Konsist can be used to automate the verification of these layers improving application code base quality and making it much more manageable. Let’s consider a few architectures and look at how we can guard their boundaries.
Challenges with Enforcing Architectural Boundaries
Imagine the application has a simple 4-layer architecture:
Here are the constraints derived from the above diagram:
presentation layer
should only access entities defined inbusiness layer
business layer
should only access entities defined inpresentation layer
andpersistence layer
persistence layer
should only access entities defined inbusiness layer
anddatabase layer
database layer
should only access entities defined inpersistence layer
Often these layers are represented as a separate package inside the Kotlin project:
All declarations (classes, interfaces, functions, etc.) defined in a given Kotlin module can be freely accessed by other declarations defined in this module — Kotlin compiler does not provide any way to define architectural boundaries when declarations exist inside a single feature module.
You may wonder why not just simply use gradle multi module to achieve the same result? The “Layer-driven modules” vs. “Feature-driven modules” are two different approaches to modularizing software. Each heaving own set of advantages and disadvantages.
Developers can easily write code that violates the intended architectural boundaries e.g. class defined in presentation layer
is directly using the interface defined in the persistence layer
skipping the business layer
:
Job Offers
Kotlin compiler will compile the code and the application will run just fine, but the architecture is broken.
It is common for developers to break architectural boundaries. Some developers may not be familiar with project architecture, others will simply make an honest mistake. This architectural violation can be obviously caught during PR review, but there is no such guarantee and often it is missed. In fact, this may be the main reason why many projects have broken architecture heaving spaghetti code. Let’s look at how Konsist can help to avoid such mistakes.
Protect 4 Tier Architecture
Konsist helps with maintaining architectural layer boundaries when multiple application layers are defined (as packages) within a single (feature) module.
Konsist can check whether each application layer uses resources only defined within permitted layers.
Firstly we need to define scope from the entire project (or the module) and use assertArchitecture
method:
Konsist .scopeFromProject() .assertArchitecture { }
Then we need to define all layers represented by individual packages:
Konsist .scopeFromProject() .assertArchitecture { val presentation = Layer("Presentation", "com.myapp.presentation..") val business = Layer("Business", "com.myapp.business..") val persistence = Layer("Persistence", "com.myapp.persistence..") val database = Layer("Database", "com.myapp.database..") }
Then we have to define dependencies between layers:
Konsist .scopeFromProject() .assertArchitecture { val presentation = Layer("Presentation", "com.myapp.presentation..") val business = Layer("Business", "com.myapp.business..") val persistence = Layer("Persistence", "com.myapp.persistence..") val database = Layer("Database", "com.myapp.database..") presentation.dependsOn(business) business.dependsOn(presentation) business.dependsOn(persistence) persistence.dependsOn(business) business.dependsOn(database) database.dependsOn(business) }
Finally, we have to wrap Konsist guard inside a unit test:
@Test fun `architecture has correct dependencies`() { Konsist .scopeFromProject() .assertArchitecture { // Define layers val presentation = Layer("Presentation", "com.myapp.presentation..") val business = Layer("Business", "com.myapp.business..") val persistence = Layer("Persistence", "com.myapp.persistence..") val database = Layer("Database", "com.myapp.database..") // Define layer dependencies presentation.dependsOn(business) business.dependsOn(presentation) business.dependsOn(persistence) persistence.dependsOn(business) business.dependsOn(database) database.dependsOn(business) } }
The above test will verify dependencies between architectural layers. This test should be executed as part of PR verification to provide a solid architectural check.
Protect Clean Architecture
The Clean Architecture is another popular architecture. One of the key concepts in this architecture is the dependency rule, which stipulates that dependencies should only point inward toward the core business logic:
In this simple 3-layer Clean Architecture, the innermost circle is the domain
layer, containing core business logic. Surrounding it are the presentation
and data
layers, which handle user interfaces and data storage respectively. Dependencies flow inward, ensuring the domain
layer remains isolated from external changes.
Here are the constraints derived from the above diagram:
domain layer
is independentpresentation layer
depends on thedomain
layerdata layer
depends on thedomain
layer
To projects such architecture using Konsist we need to define a similar test. This time we will define different dependencies between layers:
@Test fun `clean architecture layers have correct dependencies`() { Konsist .scopeFromProduction() .assertArchitecture { // Define layers val domain = Layer("Domain", "com.myapp.domain..") val presentation = Layer("Presentation", "com.myapp.presentation..") val data = Layer("Data", "com.myapp.data..") // Define layer dependencies domain.dependsOnNothing() presentation.dependsOn(domain) data.dependsOn(domain) } }
Summary
Konsist is a tool designed to ensure architectural consistency in Kotlin projects, including Android, Spring, and Kotlin Multiplatform. It verifies architectural layer boundaries, addressing common pitfalls where developers might inadvertently breach these structures. Without such checks, architectural violations might go unnoticed in PR reviews, leading to problematic code structures. By embedding Konsist’s checks in unit tests, projects can maintain and enforce their intended architecture during PR verification, benefiting both standard and Clean Architecture paradigms.
Follow me on Twitter.
Links
- Konsist project repository
- Konsist documentation
- Konsist Slack channel (at kotlinlang)
- Introducing Konsist: A Cutting-Edge Kotlin Linter
- Refactoring Multi-Module Kotlin Project With Konsist
- ArchUnit vs. Konsist. Why Did We Need Another Kotlin “Linter”?
- Konsist starter projects (GitHub)
- Android-Showcase (Android project using Konsist)
This article was previously published on proandroiddev.com