Creating a component and module structure
Declare templates, modules, libraries and bundles with the Arciphant DSL.
A simple example
Assume you have a modulithic application with 4 domain modules. In each module, you want to have a clean-architecture-like structure with the domain in the middle:
Building this structure with plain nested Gradle projects means a lot of duplicated build setup. With Arciphant you can configure a template that defines the structure of the components (rings) of a module and then use this template to create (instantiate) the 4 modules:
arciphant {
val moduleTemplate = template()
.createComponent(name = "domain")
.createComponent(name = "application", apiDependencies = setOf("domain"))
.createComponent(name = "web", dependencies = setOf("application"))
.createComponent(name = "db", dependencies = setOf("application"))
module(name = "module-a", template = moduleTemplate)
module(name = "module-b", template = moduleTemplate)
module(name = "module-c", template = moduleTemplate)
module(name = "module-d", template = moduleTemplate)
}
The above configuration creates the following structure and sets up the dependencies between the components (domain, application, web, db) in each module. How the components are mapped to Gradle depends on the configured component layout:
Every component becomes its own nested Gradle project () with the standard Gradle source sets ():
root-project
module-a
domain
src
main
kotlin
test
kotlin
testFixtures
kotlin
application
src
main
kotlin
test
kotlin
testFixtures
kotlin
web
src
main
kotlin
test
kotlin
testFixtures
kotlin
db
src
main
kotlin
test
kotlin
testFixtures
kotlin
module-b
domain
src
main
kotlin
test
kotlin
testFixtures
kotlin
application
src
main
kotlin
test
kotlin
testFixtures
kotlin
web
src
main
kotlin
test
kotlin
testFixtures
kotlin
db
src
main
kotlin
test
kotlin
testFixtures
kotlin
module-c
domain
src
main
kotlin
test
kotlin
testFixtures
kotlin
application
src
main
kotlin
test
kotlin
testFixtures
kotlin
web
src
main
kotlin
test
kotlin
testFixtures
kotlin
db
src
main
kotlin
test
kotlin
testFixtures
kotlin
module-d
domain
src
main
kotlin
test
kotlin
testFixtures
kotlin
application
src
main
kotlin
test
kotlin
testFixtures
kotlin
web
src
main
kotlin
test
kotlin
testFixtures
kotlin
db
src
main
kotlin
test
kotlin
testFixtures
kotlin
src/testFixtures is only present once the optional java-test-fixtures plugin is applied — see
Structure test code.
Every module becomes one Gradle project () whose components are source sets ():
root-project
module-a
src
domain
kotlin
domainTest
kotlin
domainTestFixtures
kotlin
application
kotlin
applicationTest
kotlin
applicationTestFixtures
kotlin
web
kotlin
webTest
kotlin
webTestFixtures
kotlin
db
kotlin
dbTest
kotlin
dbTestFixtures
kotlin
module-b
src
domain
kotlin
domainTest
kotlin
domainTestFixtures
kotlin
application
kotlin
applicationTest
kotlin
applicationTestFixtures
kotlin
web
kotlin
webTest
kotlin
webTestFixtures
kotlin
db
kotlin
dbTest
kotlin
dbTestFixtures
kotlin
module-c
src
domain
kotlin
domainTest
kotlin
domainTestFixtures
kotlin
application
kotlin
applicationTest
kotlin
applicationTestFixtures
kotlin
web
kotlin
webTest
kotlin
webTestFixtures
kotlin
db
kotlin
dbTest
kotlin
dbTestFixtures
kotlin
module-d
src
domain
kotlin
domainTest
kotlin
domainTestFixtures
kotlin
application
kotlin
applicationTest
kotlin
applicationTestFixtures
kotlin
web
kotlin
webTest
kotlin
webTestFixtures
kotlin
db
kotlin
dbTest
kotlin
dbTestFixtures
kotlin
The …Test and …TestFixtures source sets are created by default; they can be renamed or disabled — see
Component layouts for the details.
Dependency types
You probably noticed the two attributes for setting up dependencies between components:
dependencies: Creates an implementation dependency from component A to component BapiDependencies: Creates an api (transitive) dependency from component A to component B, meaning that every component depending on A gets access to B
In the above example this means that web and db can access domain thanks to the API dependency of application.
Shared code
Now assume that for each of the components (rings) you need some shared code (e.g. some shared utility for all db-components). So what you can do is create a shared library module with the same component structure. The following image shows this setup (with only two modules for simplicity):
Normally you would have to manage all these dependencies manually, which is error-prone and pollutes the Gradle build setup with repetitive code.
Arciphant instead provides an out-of-the-box mechanism: you can specify a library module:
library(name = "shared", template = moduleTemplate)
Each component of each domain module automatically gets an api dependencyapi dependencyA transitive dependency: whatever depends on the component also gets access to the shared library code.Dependency types on the same-named component in the library
module.
Complete settings.gradle.kts
arciphant {
val moduleTemplate = template()
.createComponent(name = "domain")
.createComponent(name = "application", apiDependencies = setOf("domain"))
.createComponent(name = "web", dependencies = setOf("application"))
.createComponent(name = "db", dependencies = setOf("application"))
library(name = "shared", template = moduleTemplate)
module(name = "module-a", template = moduleTemplate)
module(name = "module-b", template = moduleTemplate)
module(name = "module-c", template = moduleTemplate)
module(name = "module-d", template = moduleTemplate)
}Different shapes
Now assume that some modules (e.g. Module C and Module D) need access to a file store (e.g. MinIO). You want to put this
code into a separate component called fs like the following:
fs component.You can solve this problem by creating another template, extending from the existing template:
val moduleWithFsTemplate = template()
.extends(moduleTemplate)
.createComponent(name = "fs", dependencies = setOf("application"))
Complete settings.gradle.kts
arciphant {
val moduleTemplate = template()
.createComponent(name = "domain")
.createComponent(name = "application", apiDependencies = setOf("domain"))
.createComponent(name = "web", dependencies = setOf("application"))
.createComponent(name = "db", dependencies = setOf("application"))
val moduleWithFsTemplate = template()
.extends(moduleTemplate)
.createComponent(name = "fs", dependencies = setOf("application"))
library(name = "shared", template = moduleWithFsTemplate)
module(name = "module-a", template = moduleTemplate)
module(name = "module-b", template = moduleTemplate)
module(name = "module-c", template = moduleWithFsTemplate)
module(name = "module-d", template = moduleWithFsTemplate)
}Individual shapes
Of course, it is also possible to declare individual components in particular modules. E.g. if Module D requires the integration of an external API, you want to create a dedicated component ‘ext-api’ for the integration code:
ext-api component.To do so, you can create a component for the specific module:
module(name = "module-d", template = moduleWithFsTemplate)
.createComponent(name = "ext-api", dependencies = setOf("application"))
Complete settings.gradle.kts
arciphant {
val moduleTemplate = template()
.createComponent(name = "domain")
.createComponent(name = "application", apiDependencies = setOf("domain"))
.createComponent(name = "web", dependencies = setOf("application"))
.createComponent(name = "db", dependencies = setOf("application"))
val moduleWithFsTemplate = template()
.extends(moduleTemplate)
.createComponent(name = "fs", dependencies = setOf("application"))
library(name = "shared", template = moduleWithFsTemplate)
module(name = "module-a", template = moduleTemplate)
module(name = "module-b", template = moduleTemplate)
module(name = "module-c", template = moduleWithFsTemplate)
module(name = "module-d", template = moduleWithFsTemplate)
.createComponent(name = "ext-api", dependencies = setOf("application"))
}Extending components
A template defines which dependencies a component has by default. Sometimes a single module needs an additional
dependency between components it inherited from the template. Continuing the example above: in Module D, the web
component should also be able to use the new ext-api component. Declaring createComponent(name = "web", ...) again
would fail because the component already exists — use extendComponent instead:
module(name = "module-d", template = moduleWithFsTemplate)
.createComponent(name = "ext-api", dependencies = setOf("application"))
.extendComponent(name = "web", dependencies = setOf("ext-api"))
extendComponent adds the given dependencies and/or apiDependencies to the already declared component. Everything
else (the registered plugin and the existing dependencies) remains untouched. It is also available on templates, so a
template that extends another template can add dependencies to the inherited components as well.
Multiple templates
A module or library can also be created from several templates at once. The components of all templates are combined:
val messagingTemplate = template()
.createComponent(name = "events", dependencies = setOf("application"))
module(name = "module-e", templates = setOf(moduleTemplate, messagingTemplate))
Each component name may only be declared by one of the combined templates — combining templates that both declare a component of the same name is a configuration error.
Templates are optional, by the way: a module can also be declared without any template and consist solely of individual components:
module(name = "tooling")
.createComponent(name = "cli")
Bundle modules
Domain modules should typically remain as isolated as possible. However, to package an application we need some kind of bundle module that brings together all the modules. In Arciphant, it is possible to define bundle modules:
bundle(name = "my-app")
This creates a module my-app that has a dependency to all components of all functional modulesFunctional moduleDomain and library modules — modules that consist of components. Bundle modules are not functional modules.Concepts. The bundle module itself does not
have any components. A convention plugin (e.g. for packaging the application with Spring Boot) can be registered for a
bundle — see Bundle plugins.
It is also possible to declare explicit dependencies, e.g. if you need multiple different bundles that should not automatically depend on all functional modules:
bundle(name = "bundleX", includes = setOf(coreModuleA, coreModuleB, specificModuleX))
bundle(name = "bundleY", includes = setOf(coreModuleA, coreModuleB, specificModuleY))
Bundles can also include other bundles. So the above example could also be specified as follows to reduce duplication:
val coreBundle = bundle(name = "core", includes = setOf(coreModuleA, coreModuleB))
bundle(name = "bundleX", includes = setOf(coreBundle, specificModuleX))
bundle(name = "bundleY", includes = setOf(coreBundle, specificModuleY))
Structure test code
java-test-fixtures is a nice little Gradle plugin to deal with reusable test setup code. It basically creates an
additional source set testFixtures in between main and test. While test code should remain isolated, the idea of
testFixtures is that this code can be reused in other Gradle projects.
See official documentation for
further information.
Arciphant plays nicely with java-test-fixtures: If java-test-fixtures is applied in the Gradle build, it
automatically creates the dependencies for the testFixtures source sets between components. So if e.g. the
application component depends on domain, the testFixtures of domain will be usable in testFixtures and test
of application. Of course, if it is an api dependency, the transitivity is also taken into account.