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 and Entity 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 plain RuntimeException as previously.
  • Issue #255 fixed: return Optional on load() in Repository instances.
  • Identifiers utility class has been refactored, with a object-to-String conversion routines split into the separate Stringifiers utility class.
  • Mismatches improvements: handle the case when a command expects some value and wants to change it, but discovers a default Protobuf Message value.
  • The recommended visibility level for the command handler methods was changed to default to make the Aggregate API cleaner.
  • A utility Tests.hasPrivateUtilityConstructor() method has been renamed to Tests.hasPrivateParameterlessCtor() to better reflect broader usage.

Dependency updates:

  • Gradle 3.3.