Skip to content
Arciphant
Esc
↑↓navigate↵open⌘Jpreview
On this page

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:

Four modules with the components domain, application, web and db

Four modules sharing the same clean-architecture-like component structure.

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 B
  • apiDependencies: 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):

Two modules whose components depend on the same-named components of a shared library module

A shared library module with the same component structure (shown with 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:

Modules C and D with an additional fs component, modules A and B without

Modules C and D with an additional 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:

Module D with an individual ext-api component

Module D with an individual 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.

Debug project dependencies

Was this page helpful?