Skip to main content

Contributing

How to contribute​

Step 1: Fork​

Fork the project on GitHub and clone your fork locally.

git clone git@github.com:username/scaffold-clean-architecture.git
cd scaffold-clean-architecture
git remote add upstream https://github.com/bancolombia/scaffold-clean-architecture.git
git fetch upstream

If you haven't done it yet, configure git so that it knows who you are:

git config user.name "My name"
git config user.email "my personal or business email"
tip

You can use any name/email address you prefer here. We only use the metadata generated by git using this configuration for properly attributing your changes to you in the AUTHORS file and the changelog.

If you would like for the GitHub UI to link the commit to your account and award you the Contributor label after the changes have been merged, make sure this local email is also added to your GitHub email list.

Step 2: Hook​

We have a pre-commit hook to format the code and ensure the unit tests pass, you only need to install this with the following command.

./gradlew installGitHooks
info

If you are interested in formatting the code at any time, execute this command:

./gradlew spotlessApply

Step 3: Branch​

As a best practice to keep your development environment as organized as possible, create local branches to work within. These should also be created directly off of the master branch.

git checkout -b my-branch -t upstream/master

We suggest that the name of the branch be separated with hyphens and be concise with what you want to solve, it does not have to start with the word feature or fix.

Which branch:

  • Make your pull request target master.

Step 4: Make the changes​

You could follow this guide.


Commits​

Good commit messages serve at least three important purposes:

  • To speed up the reviewing process
  • To help us write a good release note
  • To help the future maintainers (it could be you!), say five years into the future, to find out why a particular change was made to the code or why a specific feature was added.

Rules​

  1. You should use conventional commits, you can find more information here
  2. Write the summary line and description of what you have done in the imperative mood, that is as if you were commanding someone. Start the line with "Fix", "Add", "Change" instead of "Fixed", "Added", "Changed"
  3. Contain a short description of the change (preferably 50 characters or less, and no more than 72 characters)
  4. Be entirely in lowercase with the exception of proper nouns, acronyms, and the words that refer to code, like function/variable names
  5. Keep the second line blank.
  6. If your commit introduces a breaking change (semver-major), it should contain an explanation about the reason of the breaking change, which situation would trigger the breaking change and what is the exact change.
  7. Use the body to explain what and why vs. how

For example:

test: add functional test for non-standard base libraries
ci: support for Ubuntu 18 & CentOS 7
deps: upgrade express to 4.17.1
deps: standardjs as style guide, linter, and formatter

Pull request criteria​

Requirements​

  • If you are attending an issue, add this id in your pull request.
  • Include tests
  • For new features, add the documentation in its respective section within the /docs folder.
  • Also, look at the GitHub's Pull Request guide

General info​

  • Comment on issues or pull requests.
  • Please suggest changes to documentation when you find something unclear.
  • You can create a fork of Scaffolding project in no time. Go to the GitHub project and click "Create your own fork". Create a new branch, commit your changes, and when you're ready, let us know about your pull request so we can discuss it and merge the PR!
warning

The Scaffolding project has now a continuous release bot, that means that each merged pull request will be automatically released in a newer version of the plugin. For that reason each pull request has to go through a thorough review and/or discussion.

Things we pay attention in a PR:

  • On pull requests, please document the change, what it brings, what is the benefit.
  • Clean commit history in the topic branch in your fork of the repository, even during review. That means that commits are rebased and squashed if necessary, so that each commit clearly changes one things and there are no extraneous fix-ups.
  • In the code, always test your feature / change, in unit tests.

UpgradeAction​

Required for structural changes

Implement an UpgradeAction when a structural change must also be applied to projects that were already generated by a previous release. A new v5 module that is discovered from its build.gradle does not need an upgrade action solely to add a settings include.

Add an UpgradeAction when a change to generated files must also be migrated into projects that already exist. The v5 structure is generated from templates and convention plugins; adding a module does not require an upgrade action just for Gradle to discover it.

For v5 projects, structural files include:

  • settings.gradle
  • build.gradle
  • gradle.properties
  • applications/app-service/build.gradle
  • module build.gradle files and source templates

main.gradle is a legacy file from older scaffold versions. New v5 projects do not generate it. Only reference or migrate it when supporting an existing project that still contains that file.

This upgrade action is intended to keep upgraded the projects that have been created with a previous plugin version, allowing it to be able to have the new features by simple running the gradle updateCleanArchitecture task.

The UpgradeAction should be implemented in the package co.com.bancolombia.factory.upgrades.actions and should have a specific date-based name, which allows that the update occurs in the order in which it is created.

Naming convention: UpgradeY{year}M{month}D{day}{DescriptiveName}

Where:

  • Y{year} = Year (e.g., Y2024)
  • M{month} = Month (e.g., M01 for January)
  • D{day} = Day of month (e.g., D19)
  • {DescriptiveName} = Brief description in PascalCase

Example: UpgradeY2024M01D19SonarSkipCompile

New Tasks​

If your contribution will have new tasks, you can use one of the existing abstractions, and you should use the annotation @CATask providing a name, description and a shortcut

Task Parameter Naming Convention​

When defining parameters for tasks (e.g., gradle gep --type webflux), follow these naming standards:

  • Multi-word parameters: Separate words with hyphens
    • Example: --server-name, --module-type, --base-package
  • Single-word parameters: Use without hyphens
    • Example: --type, --name, --package
tip

Consistent parameter naming improves usability and maintains a uniform CLI experience across all tasks.

New Module Factories​

If your contribution has a new item of type DrivenAdapter, EntryPoint, PerformaceTest, AcceptanceTest, Helper or Pipeline these modules factories should be named and located following the next rule.

Name: Should contain the prefix of his type, for example DrivenAdapter -> DrivenAdapterR2DBC, project will infer the driven adapter type as R2DBC

Package Location: Should be located in the corresponding package as you can see in the next table:

TypePackage
DrivenAdapterco.com.bancolombia.factory.adapters
EntryPointco.com.bancolombia.factory.entrypoints
AcceptanceTestco.com.bancolombia.factory.tests.acceptance
PerformaceTestco.com.bancolombia.factory.tests.performance
Helperco.com.bancolombia.factory.helpers
Pipelineco.com.bancolombia.factory.pipelines

Implementing or changing a module​

Introduction​

As you can research by using the project, it generates a multi-module Gradle project, each module represents a subproject that has some considerations:

  • applications:
    • app-service: Main bootable application with setup configurations.
  • domain:
    • model: Domain models for the application, it also has the ports definitions.
    • use-case: Domain business logic for the application processes.
  • infrastructure:
    • entry-points: Some modules which exposes the use-cases capabilities through an API of any protocol
    • driven-adapter: Some modules that implements the ports defined in the domain model. Could be for example an access to a database.
    • helpers: Some modules that helps the adapters, could be for example a transformation module.
  • other non gradle modules: Could be test projects, pipelines, etc...

Gradle module discovery and dependency wiring​

Scaffold v5 applies the Settings convention plugin from the generated settings.gradle. It discovers modules by finding build.gradle files under applications, domain, and infrastructure. When adding a module factory, ensure its template definition generates the module's build.gradle under one of those directories. Do not edit settings.gradle, add a static include, or call an appendToSettings helper for each module; discovery is automatic.

The root convention plugin wires detected modules into app-service by default and supplies the standard model/usecase dependencies for module types. Infrastructure implementation dependencies are also mirrored to app-service. Use excludeModuleFromApp when a module should remain in the build but not be wired into the application. Use appendDependencyToModule for additional dependencies not covered by these conventions.

Templating​

All our modules are generated based on templating using Mustache, these templates are located in the resources directory of the project.

Actually these templates should consider some variables that will impact the template, these variables are:

  • lombok: Boolean variable enabled by default, that implies that if you will use any lombok annotation you should consider implement the vanilla version when lombok is disabled.
  • metrics: Boolean variable enabled by default, that implies that you should implement some metric generator only when enabled.
  • otel: Boolean variable disabled by default; when true, the generated root build.gradle includes cleanPlugin { otel = true }.
  • reactive: Boolean variable disabled by default, this variable should be used to determine the resources directory of the templates by following the standard <module-name> for non reactive and <module-name>-reactive for reactive modules.

Predefined variables​

  • projectName: the project name assigned by the user when running the cleanArchitecture task.
  • projectNameLower: the same value of projectName but in lower case.
  • package: package defined by the user when the project was created, for example: co.com.bancolombia.sample
  • packagePath: package path of the package defined, for example: co/com/bancolombia/sample
  • All constants defined in Constants with the same constant name, for example:
    • PLUGIN_VERSION: Constants.PLUGIN_VERSION

Implementing a new module​

When you will implement a new module related to infrastructure layer you should keep in mind the required modifications.

1. Add the resource templates​

Add the resource templates in the correct location depending of your module type and the project type (reactive or not reactive).

module typeresources path
Driven Adapterresources/driven-adapter/<module-name>(-reactive)
Entry Pointresources/entry-point/<module-name>(-reactive)
Helperresources/helper/<module-name>(-reactive)
Pipelineresources/pipeline/<module-name>
Testresources/test/<module-name>

2. Implement the ModuleFactory​

Implement the ModuleFactory for the new module type in the correct package.

module typepackage
Driven Adapterco.com.bancolombia.factory.adapters
Entry Pointco.com.bancolombia.factory.entrypoints
Helperco.com.bancolombia.factory.helpers
Pipelineco.com.bancolombia.factory.pipelines
Testco.com.bancolombia.factory.tests

When you implement a new ModuleFactory which definition is:

public interface ModuleFactory {
void buildModule(ModuleBuilder builder) throws IOException, CleanException;
}

You will receive a ModuleBuilder instance which has a lot of utilities to achieve the module generation by doing operation on the project files.

Some common operations are:

setupFromTemplate​
public void setupFromTemplate(String resourceGroup) throws IOException, ParamNotFoundException {
}

This will load the template definition from the resource directory, this definition should be a json file called definition.json which has the structure:

{
"folders": [
"infrastructure/driven-adapters/dynamo-db/src/test/{{language}}/{{packagePath}}/dynamodb/helper"
],
"files": {
"driven-adapter/dynamo-db/build.gradle.mustache": "infrastructure/driven-adapters/dynamo-db/build.gradle"
}
}

Where:

  • folders: new directories to be created, usually are empty test directories.
  • files: file map to be created for Java projects.

File map refers to, in the above example for Java exists the key: driven-adapter/dynamo-db/build.gradle.mustache and the value infrastructure/driven-adapters/dynamo-db/build.gradle, it means that the file with that key will be generated in the value path. for paths value you could use variables for example infrastructure/driven-adapters/dynamo-db/src/main/{{language}}/{{packagePath}}/dynamodb/config/DynamoDBConfig.java

Template files naming convention:

When creating template files, follow these standardized naming conventions:

  • Use lowercase with hyphens: Template file names must be in lowercase and use hyphens to separate words

    • Example: handler-registry-configuration.java.mustache
    • Example: mongo-repository-adapter.java.mustache
  • Test file suffix: For test files, append .test before the file extension

    • Example: handler-registry-configuration.test.java.mustache
    • Example: commands-handler.test.java.mustache
  • Subdirectories organization: Group related templates in subdirectories when needed

    • Example: async-event-handler/metrics/metrics-reporter.java.mustache
    • Example: mongo-repository/config/mongo-config.java.mustache
tip

Consistent naming and organization improves maintainability and makes it easier for contributors to locate and understand template files. Always follow this convention when adding new modules.

appendDependencyToModule​
public void appendDependencyToModule(String module, String dependency) throws IOException {
}

This method adds an additional dependency to the indicated module. Project dependencies to app-service are generally unnecessary for discovered modules because the root convention plugin adds them automatically.

appendToProperties​
public ObjectNode appendToProperties(String path) throws IOException {
}

This method gets or creates a new ObjectNode of the application.yaml file, and you can add the custom properties to this file. For example:

The next code adds the property spring.datasource.url=jdbc:h2:mem:test and the property spring.datasource.driverClassName=org.h2.Driver

builder
.appendToProperties("spring.datasource")
.

put("url","jdbc:h2:mem:test")
.

put("driverClassName","org.h2.Driver");

3. Add the type of module in the correct enum​

module typeenum
Driven Adapterco.com.bancolombia.factory.adapters.ModuleFactoryDrivenAdapter.DrivenAdapterType
Entry Pointco.com.bancolombia.factory.entrypoints.ModuleFactory.EntryPoint
Helperco.com.bancolombia.factory.helpers.ModuleFactoryHelpers.EntryPointType
Pipelineco.com.bancolombia.factory.pipelines.ModuleFactoryPipeline.PipelineType
Testco.com.bancolombia.factory.tests.ModuleFactoryTests

4. Add the instantiation block​

Add the instantiation block in the correct class according to the new type.

module typefactory class
Driven Adapterco.com.bancolombia.factory.adapters.ModuleFactoryDrivenAdapter
Entry Pointco.com.bancolombia.factory.entrypoints.ModuleFactory
Helperco.com.bancolombia.factory.helpers.ModuleFactoryHelpers
Pipelineco.com.bancolombia.factory.pipelines.ModuleFactoryPipeline
Testco.com.bancolombia.factory.tests.ModuleFactoryTests

5. Add new required parameters in the task (Optional if required)​

For example the generic type of the driven adapter task requires a name argument, so in the task there is the option:


@Option(option = "name", description = "Set driven adapter name when GENERIC type")
public void setName(String name) {
this.name = name;
}

And then add the value to the params

builder.addParam("task-param-name",name);

Then use the param in code or template. In code:

builder.getParam("task-param-name")

Modifying a module​

You should keep in mind the above documentation for creating a new module, and basically modify the necessary templates and the corresponding module factory.


Testing in local​

1. Make the changes​

Make the changes which you want to try.

2. Change the version​

Change the version of the plugin for your custom own version in the gradle.properties file, for example:

systemProp.version=2.4.1.1

3. Publish to mavenLocal​

Publish the plugin to mavenLocal by running:

gradle publishToMavenLocal

4. Generate a new project​

Generate a new clean architecture project using the local plugin.

In an empty folder, use the plugins DSL and let pluginManagement resolve the locally published plugin:

settings.gradle
pluginManagement {
repositories {
mavenLocal()
gradlePluginPortal()
}
}
build.gradle
plugins {
id 'co.com.bancolombia.cleanArchitecture' version '2.4.1.1'
}

Replace the sample version with the version published to Maven Local, then run the respective gradle ca task with your custom arguments.

Then you can use the other tasks related to the plugin.