Posted on January 31, 2017
Spine 0.8.0
Core Java
The update 0.8.0 is aimed to extend and polish the existing framework API, making the code less repetitive and more testable. We also tried to handle the most common scenarios to reduce the number of boilerplate code in applications built on top of Spine.
Aggregate
API is extended
As learned from the real-world examples, sometimes the domain rules expressed within an Aggregate
can become an overwhelming responsibility for a single class. That’s why AggregatePart
and
AggregateRoot
abstractions are introduced to represent a large Aggregate
as a set of smaller
objects.
Such an approach also allows to access different parts of a composite Aggregate separately, improving the system performance.
Please see org.spine3.server.aggregate
package for more details.
Improvements in the testing framework
To help building the unit tests faster and do not bother with the boilerplate code, new testing
routines were introduced to org.spine3.test
package. The improvements include:
- easier testing of the commands and the entities which handle commands,
- helpers for quick creation of
Aggregate
,AggregatePart
,Projection
,ProcessManager
andEntity
instances, - routines for nullability checks.
Repackaging of Storage implementations
Storage classes were moved to the same classes as related Protobuf types. Some of the methods
were demoted from public
to protected
in order to make the API more clear.
Generated classes and interfaces are now available under storage sub-packages, and these
packages are annotated as @SPI
via package-info.java
.
Public API improvements and changes
StorageFactorySwitch
introduced
As of now it is possible to provide different StorageFactory
implementations for production and
testing modes in a unified way, taking care of the environment outside of the business and test
logic.
The Supplier
interface contract is used to allow lazy initialisation of the StorageFactory
instances and improve the app startup performance:
final Supplier<StorageFactory> productionSupplier = ...;
final Supplier<StorageFactory> testingSupplier = ...;
// ...
StorageFactorySwitch.init(productionSupplier, testingSupplier).
BoundedContext
contract changes
StorageFactory
for a BoundedContext is now provided via lazy-init Supplier
to speed up the
context instantiation.
The BoundedContext.Builder
API no longer requires StorageFactory
instance. If it not set
explicitly, the current StorageFactorySwitch
value is used. So it’s now possible to create an
instance as simple as
// Using the `StorageFactorySwitch` default.
final BoundedContext context = BoundedContext.newBuilder().build();
And one more change is made. Now the BoundedContext
class made final to clearly state it is not
designed for subclassing.
EventEnricher
and EventStore
configuration moved from BoundedContext
to EventBus
In order to make the configuration more logical and avoid potential cross-dependencies, the
configuration of the items used by the EventBus
is now available via the EventBus.Builder
,
not via the BoundedContext.Builder
:
// Spine 0.7.0:
BoundedContext.newBuilder()
.setEventStore(customEventStore)
.setEnricher(customEnricher)
//…
.build();
// Spine 0.8.0:
final EventBus eventBus = EventBus.newBuilder()
.setEventStore(customEventStore)
.setEnricher(customEnricher)
// …
.build();
BoundedContext.newBuilder()
.setEventBus(eventBus)
// …
.build();
More flexibility in event dispatching
A single Event
can now be delivered to multiple dispatchers of the same type at once.
For instance, each projection repository can be configured to route the same incoming SomeEvent
instance to a number of projections, managed by this repo. In this case each of the projection
instances will receive exactly the same SomeEvent
instance.
It is also possible to ignore the incoming Event
by returning an empty set of target projection
IDs. That may become handy to reflect a certain conditional behaviour of the read side.
This feature is available for both ProcessManagerRepository
and ProjectionRepository
.
See EventDispatchingRepository.addIdSetFunction
for details.
Dynamic configuration of the event enrichment process
Previously the EventEnricher
must have been fully initialized by the moment of the BoundedContext
creation. That made it hard to use the AggregateRepository
instances in the enrichment functions,
since AggregateRepository
required the BoundedContext
. There was a circular dependency:
EventEnricher
(wants to use) —> AggregateRepo
(requires) —> BoundedContext
(requires)
—> EventEnricher
.
In order to break the circle, it is made possible to add the new enrichment functions at runtime:
boundedContext.getEventBus().addFieldEnrichment(...)
Event enrichment features
It is now possible to specify the whole package as a target for enrichment:
message EnrichmentForAllEventsInPackage {
option (enrichment_for) = "com.foo.bar.*";
...
}
OR syntax became available in scope of by
option. It is useful when the same enrichment is
required for several events, but the anchor fields have different names:
package com.foo.bar;
// ...
message EventOne {
UserId user_id = 1;
}
message EventTwo {
UserId author_id = 1;
}
// …
message MyEnrichmentForEventOneAndTwo {
option (enrichment_for) = "com.foo.bar.*"
string name = 1 [(by) = "*.user_id | *.author_id"]
}
Postponed delivery of Events
to the subscribers and dispatchers
It is now possible to define how and when the Event
instances are delivered to their destinations.
In particular, the event propagation can be postponed until the system performance allows to
continue it.
It is especially handy for the cloud environments such as Google AppEngine. A custom strategy at a certain app node may be implemented to speed up the command propagation with no need to wait until the read-side becomes fully consistent. That allows to handle the incoming requests blazingly fast and thus scale better.
See setDispatcherEventDelivery
and setSubscriberEventDelivery
methods of EventBus.Builder
.
Postponed delivery of Entity updates to Stand
In addition to the customisable Event
delivery strategy, the same feature is available for
Entity
updates delivered to the Stand
instance. Effective on per-BoundedContext
basis,
the behaviour can be set via BoundedContext.Builder.setStandUpdateDelivery
.
See StandUpdateDelivery
documentation for details.
Valid
option changes
Valid
option has been simplified and is now a bool
instead of Message
.
// Spine 0.7.0:
MyMessage field = 1[(valid).value = true]
// Spine 0.8.0:
MyMessage field = 1[(valid) = true]
EntityRepository
has been renamed to RecordBasedRepository
This is done to reflect the class nature better.
Various improvements and fixes
UnexpectedTypeException
is thrown in response to packing/unpacking the Protobuf values instead of plainRuntimeException
as previously.- Issue #255 fixed: return
Optional
onload()
inRepository
instances. Identifiers
utility class has been refactored, with a object-to-String conversion routines split into the separateStringifiers
utility class.Mismatches
improvements: handle the case when a command expects some value and wants to change it, but discovers a default ProtobufMessage
value.- The recommended visibility level for the command handler methods was changed to
default
to make theAggregate
API cleaner. - A utility
Tests.hasPrivateUtilityConstructor()
method has been renamed toTests.hasPrivateParameterlessCtor()
to better reflect broader usage.
Dependency updates:
- Gradle 3.3.