A VLINGO XOOM toolset for implementing Actors and DDD entities/aggregates with persistence using a familiar blocking paradigm, supporting stepwise adoption to our full Reactive toolset.
A Gentle Introduction to Async
Admittedly most who refrain from the use of Reactive architectures, programming, and related tools, are generally concerned about the learning curve being too steep. Although a learning curve is indeed required, it's not as steep as most think. Whatever your expectations, we have provided a gentle path toward adopting Reactive. We think these will help you ease into Reactive.
Scooter offers a typical blocking paradigm for the purpose of gently introducing the tools and patterns generally used with DOMA and DDD.
Modeling With Entities
In a typical object-based application or service that uses POJOs, the lifecycle of entities is different from that of actor-based entities. Here we can assume that Entity and Aggregate as defined by DDD are interchangeable; that is, what follows deals with both concepts.
POJO Lifecycles
The following describes the typical lifecycle of a POJO Entity, which is what you will find when using Scooter:
Non-existing Entity states are newly constructed and then persisted to storage
Preexisting Entity states are reconstituted from storage, modified, and then persisted back to the same storage (updated).
Across a single JVM or a multi-JVM cluster, there may be any number of duplicated Entity instances. This implies that any of the duplicate instances may simultaneously have different operations performed by different users. In such cases the database must provide optimistic concurrency and detect concurrency violations via state versions.
The object reference is released after Entity persistence and the instance garbage collected.
To contrast this with the advantages of using Reactive Entities by means of XOOM Lattice, please see the related documentation.
Implementing an ObjectEntity
Implementing a concrete entity that uses object-relational mapping requires that you extend the ObjectEntity type. First create a protocol in the same manner that would be used for a corresponding Reactive entity.
To persist an instance of the EmployeeEntity's EmployeeState to the database, use it's repository. Although we provide abstract bases of both JournalRepository and StatefulRepository, we can't do so for ObjectRepository due to the numerous different ways that Object-Relational Mapping can be implemented.
Implementing Sourced Entities
Implementing a concrete entity that uses Event Sourcing requires that you extend one of the SourcedEntity base types, such as EventSourcedEntity or CommandSourcedEntity. First create a protocol in the same manner that would be used for a corresponding Reactive entity.
publicinterfaceProduct { static Product define(final String type, final String category, final String name, final String description, final long price) {
return new ProductEntity(type, category, name, description, price); }voidadjustPrice(finallong price);voidchangeDescription(finalString description);voidrename(finalString name);}
Next implement the concrete entity that implements the Product protocol.
importio.vlingo.xoom.turbo.scooter.model.sourced.EventSourcedEntity;publicclassProductEntityextendsEventSourcedEntityimplementsProduct {privateString category;privateString type;publicString name;publicString description;publiclong price; public ProductEntity(final String type, final String category, final String name, final String description, final long price) {
apply(new ProductDefined(id(), type, category, name, description, price)); } @OverridepublicStringid() {returnstreamName(); } @OverridepublicvoidadjustPrice(finallong price) {apply(new ProductPriceAdjusted(id(), price)); } @OverridepublicvoidchangeDescription(finalString description) {apply(new ProductDescriptionChanged(id(), description)); } @Overridepublicvoidrename(finalString name) {apply(new ProductRenamed(id(), name)); }// INTERNAL USE ONLY: used by repositorypublicProductEntity(finalList<Source<DomainEvent>> eventStream,finalint streamVersion) { super(eventStream, streamVersion); }protectedvoidwhenProductDefined(finalProductDefined event) {this.name=event.name;this.description=event.description;this.price=event.price; }protectedvoidwhenProductDescriptionChanged(finalProductDescriptionChanged event) {this.description=event.description; }protectedvoidwhenProductPriceAdjusted(finalProductPriceAdjusted event) {this.price=event.price; }protectedvoidwhenProductRenamed(finalProductRenamed event) {this.name=event.name; }static {registerConsumer(Product.class,ProductDefined.class, Product::whenProductDefined);registerConsumer(Product.class,ProductDescriptionChanged.class, Product::whenProductDescriptionChanged);registerConsumer(Product.class,ProductPriceAdjusted.class, Product::whenProductPriceAdjusted);registerConsumer(Product.class,ProductRenamed.class, Product::whenProductRename); }}
Note that this ProductEntity does not use an internal state type, although it could. There is an internal constructor that is to be used only by the repository, although it must be public since the implementations of ProductEntity and the repository are in two different packages. The "when" methods, which mutate the internal state of the entity as each event is applied, are registered in the static initializer and dispatched to by the base class just as they are with the corresponding Reactive entity types.
The following JournalProductRepository extends the Scooter JournalRepository and implements the ProductRepository.
Implementing a concrete entity that uses key-value/state storage requires that you extend the StatefulEntity type. First create a protocol in the same manner that would be used for a corresponding Reactive entity.
The PersonEntity uses the PersonState type as the actual persistent object, and all mutations will be reflected in transitioning that type. Note that the PersonEntity does not emit domain events when it applies new state, although it is fully supported if the implementation uses events.
publicstaticclassPersonState {publicfinalString id;publicfinalString name;publicfinalint age;publicPersonState(finalString id,finalString name,finalint age) {this.id= id;this.name= name;this.age= age; }publicPersonState(finalString id) {this(id,null,0); }publicPersonStatecopy() {returnnewPersonState(id, name, age); }publicbooleanhasState() {return id !=null&& name !=null&& age >0; }publicPersonStatewithName(finalString name) {returnnewPersonState(this.id, name,this.age); }publicPersonStatewithAge(finalint age) {returnnewPersonState(this.id,this.name, age); } @Overridepublicbooleanequals(finalObject other) {if (other ==null||other.getClass() !=this.getClass()) {returnfalse; }finalPersonState otherState = (PersonState) other;returnthis.id.equals(otherState.id); } @OverridepublicStringtoString() {return"PersonState[id="+ id +" name="+ name +" age="+ age +"]"; }}
The following PersonRepository extends the Scooter StatefulRepository.
Note specifically the uses of await in the repository implementation. This is to address the otherwise asynchronous nature of XOOM Lattice and XOOM Symbio. See the source ofStatefulRepository for uses of await(ReadInterest) and await(WriteInterest). The await(ReadInterest) returns the result of the StateStore::read() once it has completed.
Persistence
Use the JournalRepository abstract base class to implement repositories for SourcedEntity types that use Event Sourcing, Command Sourcing, or another type. You can see an implementation in the previous section.
Use the StatefulRepository abstract base class to implement repositories for StatefulEntity state types. You can see an implementation in the previous section.
Although we provide abstract bases of both JournalRepository and StatefulRepository, we can't do so for an ObjectRepository due to the numerous different ways that Object-Relational Mapping can be implemented. You may follow one of the styles exemplified in Implementing Domain-Driven Design.
Blocking Mailbox
If you consider asynchronous actor messaging to be daunting, why not trying synchronous actor messaging?
The tool provided is a specialized actor mailbox. You can read more about actors and the role of their mailboxes here and here. The XOOM Actors tooling supports any number of mailbox plugins. The particular one provided by Scooteris a blocking mailbox that requires the actor to handle the message before the sender receives control again.
This mailbox works by delivering a message to the target actor immediately rather than leaving the message for another thread to deliver. This allows you to get the feel of actor-based programming but without the unfamiliar nuances of asynchrony.
There is a potential problem with this. When you consider a request-response example, where the actor must send a message back to its sender. What would happen if the request handling actor and the response handling actor continued indefinitely sending each other messages rather than stopping after one? Correct, an ugly stack overflow would soon happen, or even re-entering an actor on the same thread and potentially modifying its state unexpectedly before the first message delivery has returned.
To prevent this the blocking mailbox does in fact implement a queue. It protects access to the queue using a compare-and-set operation. This limits the queue polling to only the first arriving enqueuing access. Think of the same thread that delivers to the request actor, next delivering a message to the response actor on the same thread. As described previously, this could cause a number of problems. So, what should be done?
Consider the above send(Message message) method of the BlockingMailbox. The mailbox invocation that occurs first locks the mailbox queue, but without blocking another attempt to lock. The compare-and-set prevents blocking on any secondary deliveries. So a second, third, forth, etc., delivery on the same thread enqueues the message, and when it sees that access is already reserved, it simply returns. When the message deliveries (method invocations) unwind, the original access will see all additional messages enqueued and deliver them. This could go on for a long time without causing issues, as long as the stack is given the opportunity to unwind before the queue causes an out-of-memory condition.
We suggest that you not plan to use the BlockingMailbox for all future development. It is provided more as a learning tool, rather than a production worthy tool. Using it in production for most or all actors gives you no advantages over a normal blocking paradigm with direct method invocations. Instead, we suggest that you use the above blocking entity types since they take advantage of strengths of direct method invocations.