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,ProcessManagerandEntityinstances, - 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
UnexpectedTypeExceptionis thrown in response to packing/unpacking the Protobuf values instead of plainRuntimeExceptionas previously.- Issue #255 fixed: return
Optionalonload()inRepositoryinstances. Identifiersutility class has been refactored, with a object-to-String conversion routines split into the separateStringifiersutility class.Mismatchesimprovements: handle the case when a command expects some value and wants to change it, but discovers a default ProtobufMessagevalue.- The recommended visibility level for the command handler methods was changed to
defaultto make theAggregateAPI cleaner. - A utility
Tests.hasPrivateUtilityConstructor()method has been renamed toTests.hasPrivateParameterlessCtor()to better reflect broader usage.
Dependency updates:
- Gradle 3.3.