Modules
Introduction
Enabling and ensuring modularity is a key principle for the Apache Causeway framework. Modularity is the only way to ensure that a complex application domain does not over time degenerate into the infamous "big ball of mud", software that is difficult, dangerous and expensive to change.
Modules chunk up the overall application into smaller pieces, usually a package with subpackages. The smaller pieces can be either:
-
horizontal tiers (presentation / application / domain / persistence) or
-
vertical functional slices (eg customer vs orders vs products vs invoice etc).
Because Apache Causeway takes care of the presentation and persistence tiers, modules for us correspond to vertical functional slices. The framework is intended to support complex domains, which we tackle by breaking that domain down into smaller subdomains, in other words into modules.
It could be argued that there is still an application tier (view models) and a domain tier (entities) to be considered, as well as mixins. We describe the structure of an archetype module below, showing one way of organising the horizontal tiers within the vertical slices.
Having broken the application down into smaller modules, these modules will then depend upon each other. The two main rule of thumbs for dependencies are:
-
there should be no cyclic dependencies (the module dependencies should form an acyclic graph), and
-
unstable modules should depend upon stable modules, rather than the other way around.
By "unstable" we don’t mean buggy, rather this relates to its likelihood to change its structure or behaviour over time: in other words its stability as a core set of concepts upon which other stuff can depend. Reference data (calendars, tax rates, lookups etc) are generally stable, as are "golden" concept such as counterparties / legal entities or financial accounts. Transactional concepts such as invoices or agreements is perhaps more likely to change. But this stuff is domain specific.
Programming Model
In Apache Causeway we use Spring @Configurations to define a module, consisting of a set of domain services and domain objects (entities, view models and mixins). Spring’s @Import is used to express a dependency between each "configuration" module.
For example:
package com.mycompany.modules.customer;
...
@Configuration (1)
@Import({
CausewayModuleExtExcelApplib.class (2)
})
@ComponentScan (3)
@EntityScan (4)
public class CustomerModule {}
1 | this is a module |
2 | dependency on another module, in this case the Excel module provided by Apache Causeway itself. |
3 | scan for domain services and objects etc under this package (eg a Customer entity or a CustomerRepository domain service). |
4 | scan for domain entities. This is required in applications that use JPA/EclipseLink as their ORM. |
See Spring documentation on annotation-based container configuration, classpath scanning and java-based configuration for much more on this topic. |
Since @Configuration
and @ComponentScan
often appear together, Apache Causeway provides a convenience @Module
annotation, which is meta-annotated with both.
The above example could therefore be rewritten as:
package com.mycompany.modules.customer;
...
import org.apache.causeway.applib.annotation.Module;
@Module
@Import({
CausewayModuleExtExcelApplib.class
})
@EntityScan
public class CustomerModule {}
Maven modules
By convention, we have just one Spring module to each Maven module.
This means that the <dependency>`s between Maven modules are mirrored in the Spring module’s `@Import
statements).
We can therefore rely on Maven to ensure there are no cyclic dependencies: the application simply won’t compile if we introduce a cycle.
If the above convention is too officious, then you could choose to have multiple Spring modules per Maven module, but you will need to watch out for cycles. In such cases (proprietary) tools such as Structure 101 can be used to help detect and visualize such cycles. Or, (open source) libraries such as ArchUnit or jQAssistant can help enforce architectural layering to prevent the issue arising in the first place. (These tools can enforce other conventions, too, so are well worth exploring). |
We recommend a single top-level package corresponding to the module, aligned with the <groupId>
and <artifactId>
of the Maven module in which it resides.
This top-level package is also where the Spring @Configuration
module class resides.
Apache Causeway’s own modules follow this convention. For example, the Excel extension module has several submodules, one of which is its applib:
-
its Maven
groupId:artifactId
is:org.apache.causeway.extensions:causeway-extensions-excel-applib
-
in the applib, its top-level package is:
org.apache.causeway.extensions.excel.applib
-
it defines a Spring configuration module called:
CausewayModuleExtExcelApplib
which looks like:
@Configuration @ComponentScan (1) @EntityScan (2) public class CausewayModuleExtExcelApplib { }
1 the @ComponentScan indicates that the classpath should be scanned for domain services, entities and fixture scripts. 2 recommended if using JPA/EclipseLink, skip if using JDO.
When there is a dependency, this is then expressed in two ways: first, as a "physical" <dependency>
in Maven; and second, as a "logical" dependency using @Import in the @Configuration
module.
Looking again at the Excel library once more, this has an applib
module and also a testing
(artifactId of causeway-extensions-excel-testing
), where:
Therefore:
-
in the testing module’s
pom.xml
, we see:<dependency> <groupId>org.apache.causeway.extensions</groupId> <artifactId>causeway-extensions-excel-applib</artifactId> </dependency>
-
and in the testing module’s
CausewayModuleExtExcelTesting
we see:@Configuration @Import({ (1) CausewayModuleExtExcelApplib.class }) @ComponentScan public class CausewayModuleExtExcelTesting { }
1 The @Import annotation declares the dependency.
Unlike Maven, there is no distinction in Spring between production (src/main/java
) and test (src/test/java
) classpath dependencies.
But if the physical classpath dependency is missing, then an incorrectly defined @Import
will simply not compile.
AppManifest
There needs to be one top-level module that references all of the modules that make up the application, either directly or indirectly (through transitive dependencies).
In Apache Causeway we call this module the "app manifest" or AppManifest
, though this is just a name: it is, ultimately, just another Spring @Configuration
module.
For example, in the simpleapp starter app the AppManifest
looks like:
package domainapp.webapp;
//...
@Configuration
@Import({
CausewayModuleApplibMixins.class,
CausewayModuleApplibChangeAndExecutionLoggers.class,
CausewayModuleCoreRuntimeServices.class,
CausewayModulePersistenceJpaEclipselink.class,
CausewayModuleViewerRestfulObjectsJaxrsResteasy.class,
CausewayModuleViewerWicketApplibMixins.class,
CausewayModuleViewerWicketViewer.class,
CausewayModuleTestingFixturesApplib.class,
CausewayModuleTestingH2ConsoleUi.class,
CausewayModuleExtFlywayImpl.class,
CausewayModuleExtSecmanPersistenceJpa.class,
CausewayModuleExtSecmanEncryptionSpring.class,
CausewayModuleExtSessionLogPersistenceJpa.class,
CausewayModuleExtAuditTrailPersistenceJpa.class,
CausewayModuleExtCommandLogPersistenceJpa.class,
CausewayModuleExtExecutionLogPersistenceJpa.class,
CausewayModuleExtExecutionOutboxPersistenceJpa.class,
CausewayModuleExtExcelDownloadWicketUi.class,
CausewayModuleExtFullCalendarWicketUi.class,
CausewayModuleExtPdfjsWicketUi.class,
CausewayModuleValAsciidocMetaModel.class,
CausewayModuleValAsciidocUiWkt.class,
ApplicationModule.class,
CustomModule.class,
QuartzModule.class,
// discoverable fixtures
DomainAppDemo.class
})
@PropertySources({
@PropertySource(CausewayPresets.DebugDiscovery), (1)
})
public class AppManifest {
}
1 | there are a whole set of preset configuration properties that you can enable |
Decoupling
Having broken up a domain into multiple modules, there is still a need for higher level modules to use lower level modules, and the application must still appear as a coherent whole to the end-user.
The key features that Apache Causeway provides to support this are:
-
dependency injection of services
Both framework-defined domain services and application-defined services (eg repositories and factories) are injected everywhere, using the
@javax.inject.Inject
annotation (Spring’s@Autowired
can also be used).By "everywhere", we mean not just into domain services, but also can be injected into domain entities, view models and mixins. This enables us to implement behaviourally complete domain objects (if we so wish).
-
mixins that allow functionality defined in one module to appear (in the UI) to be provided by some other module.
For example, a Document module might allow
Document
objects to be attached to any arbitrary domain object (such asOrder
orCustomer
) in other modules. A mixin would allow the UI for aCustomer
to also display these attachedDocument
s, even though the Customer module would have no knowledge of/dependency on the Workflow module. (More on this example below).Dependencies are also injected into mixins. A common technique is to factor out from domain objects into mixins and then generalise.
-
events allow modules to influence other modules.
A subscriber in one module can subscribe to events emitted by domain objects in another module. These events can affect both the UI (eg hiding or disabling object members, or allowing or vetoing interactions).
Using events we can implement referential integrity constraints within the application.
For example, suppose the customers
module has a Customer
object and a EmailAddress
object, with a customer having a collection of email addresses.
A communications
module might then use those email addresses to create EmailCommunication
s.
customers
module and communications
moduleIf the customers
module wants to delete an EmailAddress
then the communications
module will probably want to veto this because they are "in use" by those EmailCommunication
s.
Or, it might conceivably perform a cascade delete of all associated communications.
Either way, the communications
module receives an internal event representing the intention to delete the EmailAddress
.
It can then act accordingly, either vetoing the interaction or performing the cascade delete.
The customers
module for its part does not know anything about this other module.
Inverting Dependencies
If we get the dependencies wrong (that is, our initial guess on stability is proven incorrect over time) then there are a couple of techniques we can use:
-
use the dependency inversion principle to introduce an abstraction representing the dependency.
-
move functionality, eg by factoring it out into mixins into the other module or into a third module which depends on the other modules that had a bidirectional relationship
Mixins in particular allow dependencies to be inverted, so that the dependencies between modules can be kept acyclic and under control.
For example, suppose that we send out Invoice
s to Customer
s.
We want the invoices to know about customers, but not vice versa.
We could surface the set of invoices for a customer using a Customer_invoices
mixin:
invoices
module contributes to customers
In the UI, when rendering a Customer
, we would also be presented with the associated set of Invoice
s.
We can also use mixins for dependencies that are in the other direction.
For example, suppose we have a mechanism to attach Document
s to arbitrary domain objects.
The documents module does not depend on any other modules, but provides a DocumentHolder
marker interface.
We can therefore attach documents to a Customer
by having Customer
implement this marker interface:
customers
depends upon contributions of documents
An Archetypal Module
As was discussed in the introductory section above, in Apache Causeway a module corresponds to a vertical slice of functionality. But how should we organise the horizontal tiers (application vs domain) within the module? And how does one module interact with another module?
The diagram below shows a set of packages for a module, for the different concerns:
Pkg | Pkg name | Description |
---|---|---|
|
Domain object model |
Holds entities, repositories, supporting services and "local" mixins that act upon those entities |
|
Application layer |
Holds view models and "manager" objects that describe a business process |
|
Menu services |
|
|
REST API |
|
|
Application Program Interface |
|
|
Application library |
Public interfaces implemented by domain objects in |
|
Contributions |
Mixins in this module that contribute to other modules.
These mixins will use the domain objects in |
|
Service Provider Interface |
Defines hooks that allow other modules to influence behaviour of this module.
Called by objects in |
|
SPI Implementations |
Implementations in this module of SPIs defined in other modules.
These SPI implementations will likely use the domain object functionality defined in |
The following diagram extends the previous, this time showing the interactions between modules:
In the above we see that:
-
shipping
makes programmatic calls into theapi
ofordermgmt
-
ordermgmt
defines anspi
that is implemented bywarehouse
Presumably
spiimpl
ofordermgmt
would implement thespi
of some other module, not shown. -
ordermgmt
's mixins incontrib
contribute behaviour to all objects that implement the interfaces defined incustomer
'sapplib
. -
ordermgmt`s domain objects in `dom
implement the interfaces inapplib
, meaning that other modules can contribute to them.They might also implement from the
applib
of other modules (eg aCustomer
declaring itself to be aDocumentHolder
).