SeedStack is an extensible solution that can be enriched with add-ons to provide new functionality or features. Writing an add-on is not a difficult task as it is very similar to writing an application but some rules and conventions must be obeyed. This guide will describe these rules and conventions.

Project structure

An add-on almost always consists in an API/SPI with one or more implementations. Depending on the complexity of the add-on and the re-usability requirements, several project structures can be used.

Single-module add-on

The simplest form of add-on is a single module project. In this case, the API/SPI and the implementation will be contained in a single JAR artifact. The typical single-module add-on structure is:

single-addon
    |- src/main/java
    |   |- org.myorg.feature    <-- API goes in the add-on base package
    |       |- internal         <-- Implementation            
    |       |- spi              <-- SPI if any
    |- src/test/java
        ...

Example single module add-on: https://github.com/seedstack/jpa-addon

Add-on with a separated API/SPI

When you need to provide the API/SPI to clients separately from the implementation you need to create a multi-module add-on:

  • The specs module will contain the API (and the SPI if any),
  • The core module will contain the implementation.

The typical structure for such add-on is:

multi-addon
    |- core
    |   |- src/main/java
    |   |   |- org.myorg.feature    
    |   |       |- internal         <-- Implementation            
    |   |- src/test/java
    |       ...
    |- specs        
    |   |- src/main/java
    |   |   |- org.myorg.feature    <-- API goes in the add-on base package
    |   |       |- spi              <-- SPI if any
    |   |- src/test/java
            ...

Java packages are still the same that found in the single module add-on but segregated in two maven modules.

Multiple implementations add-on

When you have multiple implementations or implementation extensions for an add-on you can add a new sub-module per implementation/extension. This type of add-on is the same a the previous one with additional sub-modules:

  • The specs module will contain the API (and the SPI if any),
  • The core module will contain the main/common implementation,
  • The impl1 module will contain the impl1 implementation,
  • The impl2 module will contain the impl2 implementation,
multi-addon
    |- core
    |   |- src/main/java
    |   |   |- org.myorg.feature    
    |   |       |- internal         <-- Main/common implementation            
    |   |- src/test/java
    |       ...
    |- impl1
    |   |- src/main/java
    |   |   |- org.myorg.feature.impl1    
    |   |       |- internal         <-- Implementation/extension 1            
    |   |- src/test/java
    |       ...
    |- impl2
    |   |- src/main/java
    |   |   |- org.myorg.feature.impl2    
    |   |       |- internal         <-- Implementation/extension 2            
    |   |- src/test/java
    |       ...
    |- specs        
    |   |- src/main/java
    |   |   |- org.myorg.feature    <-- API goes in the add-on base package
    |   |       |- spi              <-- SPI if any
    |   |- src/test/java
            ...

If you don’t have any API, like when it is provided by a third party, you can omit the specs module completely.

Example multi-module add-on: https://github.com/seedstack/i18n-addon

Dependencies

As the add-on is a reusable component which will be used in various contexts, the rules on dependencies are tighter than on applications.

As a general rule, try to minimize the number of dependencies in your add-on to help avoid unintended side-effects and limit its impact on client projects.

  • Any dependency that will be provided by the client project or its runtime environment must be specified with a provided scope. The meaning here is that the dependency is required for proper operation but must be provided downstream.
  • If your add-on contains optional features that have dependencies you have two options:
    • Either package those features in their own implementation sub-module with their own dependencies,
    • Or package those features in the core implementation, mark their dependencies as optional and use conditional code to initialize those features only when their dependency requirements are met.

API/SPI

All API classes go in the base package of the add-on. All SPI classes go in a spi subpackage of the base package. Try to keep the API classes as flat as possible, ideally without subpackage.

Implementation

All implementation classes go in a *.internal.* which is completely excluded from backwards compatibility requirements between versions.