Skip to content

Commit

Permalink
GH-6 - More reference documentation.
Browse files Browse the repository at this point in the history
  • Loading branch information
odrotbohm committed Oct 13, 2022
1 parent c81a0b5 commit 80deb08
Show file tree
Hide file tree
Showing 6 changed files with 211 additions and 14 deletions.
71 changes: 69 additions & 2 deletions src/docs/asciidoc/10-fundamentals.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Thus, naturally, the module's API consists of all public types in the package.

Let us have a look at an example arrangement (icon:plus-circle[] denotes a public type, icon:minus-circle[] a package protected one).

.A single inventory application module
[source, subs="macros"]
----
icon:cubes[] Example
Expand All @@ -48,7 +49,8 @@ icon:cubes[] Example

If an application module package contains sub-packages, types in those might need to be made public so that it can be referred to from code of the very same module.

[source, subs="macros"]
.An inventory and order application module
[source, subs="macros, quotes"]
----
icon:cubes[] Example
└─ icon:folder[] src/main/java
Expand All @@ -57,7 +59,7 @@ icon:cubes[] Example
├─ icon:cube[] example.inventory
| ├─ icon:plus-circle[] InventoryManagement.java
| └─ icon:minus-circle[] SomethingInventoryInternal.java
├─ icon:cube[] example.order
├─ **icon:cube[] example.order**
| └─ icon:plus-circle[] OrderManagement.java
└─ icon:cube[] example.order.internal
└─ icon:plus-circle[] SomethingOrderInternal.java
Expand All @@ -70,24 +72,43 @@ Code within those must not be referred to from other modules.
Note, how `SomethingOrderInternal` is a public type, likely because `OrderManagement` depends on it.
This unfortunately means, that it can also be referred to from other packages such as the `inventory` one.

[[fundamentals.modules.explicit-dependencies]]
=== Explicit Application Module Dependencies
A module can opt into declaring its allowed dependencies by using the `@ApplicationModule` annotation on the `package-info.java` type.

.Inventory explicitly configuring module dependencies
[source, java]
----
@org.springframework.modulith.ApplicationModule(
allowedDependencies = "order"
)
package example.inventory;
----

In this case code within the __inventory__ module was only allowed to refer to code in the __order__ module (and code not assigned to any module in the first place).
Find out about how to monitor that in <<verification>>.

[[fundamentals.modules.application-modules]]
=== The `ApplicationModules` Type

Spring Moduliths allows to inspect a codebase to derive an application module model based on the given arrangement and optional configuration.
The `spring-modulith-core` artifact contains `ApplicationModules` that can be pointed to a Spring Boot application class:

.Creating an application module model
[source, java]
----
var modules = ApplicationModules.of(Application.class);
----

To get an impression about what the analyzed arrangement looks like, we can just write the individual modules contained in the overall model to the console:

.Writing the application module arranagement to the console
[source, java]
----
modules.forEach(System.out::println);
----

.The console output of our application module arrangement
[source]
----
## example.inventory ##
Expand All @@ -109,3 +130,49 @@ Note, how each module is listed and the contained Spring components are identifi

[[fundamentals.modules.named-interface]]
=== Named Interfaces

By default and as described in <<fundamentals.modules.advanced>>, an application module's base package is considered the API package and thus is the only package to allow incoming dependencies from other modules.
In case you would like to expose additional packages to other modules, you need to use __named interfaces__.
You achieve that by annotating the `package-info.java` file of those package with `@NamedInterface`.

.A package arrangement to encapsulate an SPI named interface
[source, text, subs="macros, quotes"]
----
icon:cubes[] Example
└─ icon:folder[] src/main/java
├─ icon:cube[] example
| └─ icon:plus-circle[] Application.java
├─ …
├─ icon:cube[] example.order
| └─ icon:plus-circle[] OrderManagement.java
├─ **icon:cube[] example.order.spi**
| ├— icon:coffee[] package-info.java
| └─ icon:plus-circle[] SomeSpiInterface.java
└─ icon:cube[] example.order.internal
└─ icon:plus-circle[] SomethingOrderInternal.java
----

.`package-info.java` in `example.order.spi`
[source, java]
----
@org.springframework.modulith.NamedInterface("spi")
package example.order.spi;
----

The effect of that declaration is two fold: first, code in other application modules is allowed to refer to `SomeSpiInterface`.
Application modules are able to refer to the named interface in explicit dependency declarations.
Assume the __inventory__ module was making use of that, it could refer to the above declared named interface like this:

[source, java]
----
@org.springframework.modulith.ApplicationModule(
allowedDependencies = "order::spi"
)
package example.inventory;
----

Note how we concatenate the named interface's name `spi` via the double colon `::`.
In this setup, code in __inventory__ would be allowed to depend on `SomeSpiInterface` and other code residing in the `order.spi` interface, but not on `OrderManagement` for example.
For modules without explicitly described dependencies, both the application module root package *and* the SPI one are accessible.


7 changes: 5 additions & 2 deletions src/docs/asciidoc/20-verification.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ ApplicationModules.of(Application.class).verify();
The verification includes the following rules:

* _No cycles on the application module level_ -- the dependencies between modules have to form directed, acyclic graph.
* _Efferent module access via API packages only_ -- All references to types that reside in application module internal packages are rejected. See <<fundamentals.modules.advanced>> for details.
* _Explicitly allowed application module dependencies only_ (optional) -- An application module can optionally define allowed dependencies via `@ApplicationModule(allowedDependencies = …)`. If those are configured, dependencies to other application modules are rejected.
* _Efferent module access via API packages only_ -- All references to types that reside in application module internal packages are rejected.
See <<fundamentals.modules.advanced>> for details.
* _Explicitly allowed application module dependencies only_ (optional) -- An application module can optionally define allowed dependencies via `@ApplicationModule(allowedDependencies = …)`.
If those are configured, dependencies to other application modules are rejected.
See <<fundamentals.modules.explicit-dependencies>> and <<fundamentals.modules.named-interfaces>> for details.

Spring Modulith optionally integrates with the jMolecules ArchUnit library and, if present, automatically triggers its verification rules described https://github.com/xmolecules/jmolecules-integrations/tree/main/jmolecules-archunit[here].
11 changes: 9 additions & 2 deletions src/docs/asciidoc/30-testing.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,23 @@
Spring Modulith allows to run integration tests bootstrapping individual application modules in isolation or combination with others.
To achieve this, place JUnit test class in an application module package or any sub-package of that and annotate it with `@ApplicationModuleTest`:

.A application module integration test class
[source, java]
----
package example.order;
@ApplicationModuleTest
class OrderIntegrationTests {
// Individual test cases go here
}
----

This will run you integration test similar to what `@SpringBootTest` would have achieved but with the bootstrap actually limited to the application module the test resides in.
If you configure the log level for `org.springframework.modulith` to `DEBUG`, you will see detailed information about how the test execution customizes the Spring Boot bootstrap:

[source]
.The log output of a application module integration test bootstrap
[source, text, subs="macros"]
----
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
Expand All @@ -36,10 +41,11 @@ If you configure the log level for `org.springframework.modulith` to `DEBUG`, yo
… - + ….internal.OrderInternal
… - Starting OrderIntegrationTests using Java 17.0.3 …
… - No active profile set, falling back to 1 default profile: "default"
… - Re-configuring auto-configuration and entity scan packages to: example.order.
… - pass:quotes[**Re-configuring auto-configuration and entity scan packages to: example.order.**]
----

Note, how the output contains the detailed information about the module included in the test run.
It creates the application module module, finds the module to be run and limits the application of auto-configuration, component and entity scanning to the corresponding packages.

[[testing.bootstrap-modes]]
== Bootstrap Modes
Expand All @@ -57,6 +63,7 @@ When an application module is bootstrapped, the Spring beans it contains will be
If those contain bean references that cross module boundaries, the bootstrap will fail if those other modules are not included in the test run (see <<testing.bootstrap-modes>> for details).
While a natural reaction might be to expand the scope of the application modules included, it is usually a better option to mock the target beans.

.Mocking Spring bean dependencies in other application modules
[source, java]
----
@ApplicationModuleTest
Expand Down
50 changes: 49 additions & 1 deletion src/docs/asciidoc/40-events.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ The `complete(…)` method creates functional gravity in the sense that it attra
This especially makes the component harder to test as we need to have instances available of those depended on beans just to create an instance of `OrderManagement` (see <<testing.efferent-dependencies>>).
It also means that we will have to touch the class whenever we would like to integrate further functionality with the business event order completion.

We can change the application module interaction as follows:

.Publishing an application event via Spring's `ApplicationEventListener`
[source, java]
----
@Service
Expand All @@ -49,12 +52,14 @@ public class OrderManagement {
}
----

Note, how we use Spring's `ApplicationEventPublisher` to publish a domain event, once we have completed the state transitions on the primary aggregate.
Note, how, instead of depending on the other application module's Spring bean, we use Spring's `ApplicationEventPublisher` to publish a domain event, once we have completed the state transitions on the primary aggregate.
For a more aggregate-driven approach to even publication, see https://docs.spring.io/spring-data/data-commons/docs/current/reference/html/#core.domain-events[Spring Data's application event publication mechanism] for details.
As event publication happens synchronously by default, the transactional semantics of the overall arrangement stay the same as in the example above.
Both for the good, as we get to a very simple consistency model (either both the status change of the order _and_ the inventory update succeed or none of them does), but also for the bad as more triggered related functionality will widen the transaction boundary and potentially cause the entire transaction to fail, even if the functionality that is causing the error is not crucial.

A different way of approaching this is by moving the event consumption to asynchronous handling at transaction commit and treat secondary functionality exactly as that:

.An async, transactional event listener
[source, java]
----
@Service
Expand Down Expand Up @@ -117,3 +122,46 @@ The following starters are available:
* `spring-modulith-starter-jdbc` -- Using JDBC as persistence technology. Also works in JPA-based applications but bypasses your JPA provider for actual event persistence.
* `spring-modulith-starter-mongodb` -- Using MongoDB behind Spring Data MongoDB.

[[events.integration-testing]]
== Integration Testing Application Modules Workting with Events

Integration tests for application modules that interact with other modules' Spring beans usually have those mocked and the test cases verify the interaction by verifying that that mock bean was invoked in a particular way.

.Traditional integration testing of the application module interaction
[source, java, subs="quotes"]
----
@ApplicationModuleTest
class InventoryIntegrationTests {
**@MockBean SomeOtherComponent someOtherComponent;**
@Test
void someTestMethod() {
// Given
// When
// Then
**verify(someOtherComponent).someMethodCall();**
}
}
----

In an event-based application interaction model, the dependency to the other application module's Spring bean is gone and we have nothing to verify.
Spring Modulith's `@ApplicationModuleTest` enables the ability to get a `PublishedEvents` instance injected into the test method to verify a particular set of events has been published during the course of the business operation under test.

.Event-based intergration testing of the application module arrangement
[source, java, subs="quotes"]
----
@ApplicationModuleTest
class InventoryIntegrationTests {
@Test
void someTestMethod(**PublishedEvents events**) {
// Given
// When
// Then
**assertThat(events.…).…;**
}
}
----
84 changes: 78 additions & 6 deletions src/docs/asciidoc/60-documentation.adoc
Original file line number Diff line number Diff line change
@@ -1,9 +1,81 @@
[[documentation]]
= Documenting Application Modules

[[documentation.subheadline]]
== Subheadline
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
The application module model created via `ApplicationModules` can be used to create documentation snippets for inclusion into developer documentation written in Asciidoc.
Spring Modulith's `Documenter` abstraction can produce two different kinds of snippets:

* C4 and UML component diagrams describing the relationships between the individual application modules
* A so called __Application Module Canvas__, a tabular overview about the module and the most relevant elements in those (Spring beans, aggregate roots, events published and listened to as well as configuration properties).

[[documentation.component-diagrams]]
== Generating Application Module Component diagrams

The documentation snippets can be generated by handing the `ApplicationModules` instance into a `Documenter`.

.Generating application module component diagrams using `Documenter`
[source, java]
----
class DocumentationTests {
ApplicationModules modules = ApplicationModules.of(Application.class);
@Test
void writeDocumentationSnippets() {
new Documenter(modules)
.writeModulesAsPlantUml()
.writeIndividualModulesAsPlantUml();
}
}
----

The first call on `Documenter` will generate a C4 component diagram containin all modules within the system.
The second call will create additional diagrams that only include the individual module and the ones they directly depend on on the canvas.

[[documentation.component-diagrams.uml]]
=== Using Traditional UML Component Diagrams

If you prefer the traditional UML style component diagrams, tweak the `DiagramOptions` to rather use that style as follows:

[source, java]
----
DiagramOptions.defaults()
.withStyle(DiagramStyle.UML);
----

This will cause the diagrams to look like this:

TODO: Add diagram

[[documentation.application-module-canvas]]
== Generating Application Module Canvases

The Application Module Canvases can be generated by calling `Documenter.writeModuleCanvases()`:

.Generating application module component diagrams using `Documenter`
[source, java]
----
class DocumentationTests {
ApplicationModules modules = ApplicationModules.of(Application.class);
@Test
void writeDocumentationSnippets() {
new Documenter(modules)
.writeModuleCanvases();
}
}
----

A canvas generated looks like this:

TODO: Include generated canvas

It consists of the following sections:
* __The application module's name and base package.__
* __The Spring beans exposed by the application module, grouped by stereotype.__ -- In other words beans that are located in either the API package or any <<fundamentals.modules.named-interface, named interface package>>.
* __Exposed aggregate roots__
* __Application events published by the module__ -- Those event types need to be demarcated using jMolecules `@DomainEvent` or implement its `DomainEvent` interface.
* __Application events listened to by the module__ -- Derived from methods annotated with Spring's `@EventListener`, `@TransactionalEventListener` or beans implementing `ApplicationListener`.
* __Configuration properties__ -- Requires the usage of the `spring-boot-configuration-processor` artifact to extract the metadata attached to the properties.
2 changes: 1 addition & 1 deletion src/docs/asciidoc/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ NOTE: Copies of this document may be made for your own use and for distribution

:leveloffset: +1

include::00-preface.adoc[]
// include::00-preface.adoc[]

include::10-fundamentals.adoc[]

Expand Down

0 comments on commit 80deb08

Please sign in to comment.