Skip to content

Intro to Common Domain

haf edited this page Jul 12, 2011 · 4 revisions

Overview

The Common Domain project is subdivided into the following projects:

CommonDomain – Interfaces are defined here: IAggregate, IDetectConflicts, IMemento, IRouteEvents, ISaga. Use this assembly together with your domain to be able to use the default infrastructure implementations of persistence, committed and uncommitted events etc.

CommonDomain.Core – Implementations for the aforementioned interfaces, allowing your to get a nice start towards implementing the interfaces by inheritance.

CommonDomain.Persistence – Defines interfaces (IRepository, ISagaRepository, IConstructAggregates) that an event store may implement, and two exceptions for optimistic concurrency violations (by version) or persistence problems.

CommonDomain.Persistence.EventStore – Implements the above interfaces.

CommonDomain assembly

Location for some interfaces, the aggregate, which defines a bounded context and consistency boundary:

public interface IAggregate
{
	Guid Id { get; }
	int Version { get; }

	void ApplyEvent(object @event);
	ICollection GetUncommittedEvents();
	void ClearUncommittedEvents();

	IMemento GetSnapshot();
}

The conflict detector, which allows you to choose when two commands cause conflicting events:

public interface IDetectConflicts
{
	void Register<TUncommitted, TCommitted>(ConflictDelegate handler)
		where TUncommitted : class
		where TCommitted : class;

	bool ConflictsWith(IEnumerable<object> uncommittedEvents, IEnumerable<object> committedEvents);
}

public delegate bool ConflictDelegate(object uncommitted, object committed);

The memento, used to take (often async) snapshots of your aggregate root to load it quicker:

public interface IMemento
{
	Guid Id { get; set; }
	int Version { get; set; }
}

The event router, which :

public interface IRouteEvents<TEvent>
{
	void Register<TEventMessage>(Action<TEventMessage> handler) where TEventMessage : TEvent;
	void Register(IAggregate aggregate);

	void Dispatch(object eventMessage);
}

and finally the saga:

public interface ISaga
{
	Guid Id { get; }
	int Version { get; }

	void Transition(object message);

	ICollection GetUncommittedEvents();
	void ClearUncommittedEvents();

	ICollection GetUndispatchedMessages();
	void ClearUndispatchedMessages();
}

CommonDomain.Core assembly

The AggregateBase implementation handles transparent dispatching to Apply(TE e)-methods, where TE is the event type that the aggregate is applying. It keeps track of committed and uncommitted events.

The ConflictDetector is used to determine if the events to be committed represent a true business conflict as compared to events that have already been committed, thus allowing reconciliation of optimistic concurrency problems.

The ConventionEventRouter&lt;TEvent&gt; calls, by convention, the Apply(TEvent e) method on the aggregate root when replaying the event log. It's the default and can be contrasted with RegistrationEventRouter&lt;TEvent&gt; which requires a manual event handler registration.

The SagaBase defines some common concepts around sagas.

CommonDomain.Persistence

This assembly defines an exception thrown if two commands cannot be reconsolidated, the ConflictingCommandException, which represents a command that could not be executed because it conflicted with the command of another user or actor. This exception should be caught in command handlers to sent back to the user interface for handling, or by invoking a compensatory action.

Further, it contains an interface for creating aggregate roots; IConstructAggregates, and IRepository that is the blue-print for event-store-based repositories.

IConstructAggregates is used by the CommonDomain.Persistence.EventStore assembly - the EventStoreRepository implementation takes such an instance in its constructor, for example.

ISagaRepository specifies an interface to load sagas by Id and save them back to persistent medium.

CommonDomain.Persistence.EventStore

By implementing the interfaces defined in the other assemblies, this assembly ties together the EventStore project with the CommonDomain project by providing very sane implementations of the storage interfaces.

One of the most important methods is the void Save(IAggregate aggregate, Guid commitId, Action<IDictionary<string, object>> updateHeaders) method. The EventStoreRepository reacts to broken invariants, in these ways, when committing an IAggregate's uncommitted events to the storage medium:

  • DuplicateCommitException - clears the changes to be made an returns (footnote: the idempotency requirement for applied events if network failed before the ACK of the previous write came in, corollary in SQL-world would be to handle the TransactionInDoubtException on application/server startup, but with this version of the logic, that's not needed since we're only acting on an AR-Id). This exception never gets rethrown from the default implementation.
  • ConcurrencyException - if another command or otherwise aggregate-mutating message has been written before the current save method's invocation context - e.g. by unreliable network or message reordering - this exception is thrown. The repository handles it by checking with the IDetectConflicts instance, whether the n events are conflicting such. If they are, the exception ConflictingCommandException gets thrown, otherwise the changes caused by the saved events are cleared and ignored - in this case the changes implied by the events need to be further handled in the IDetectConflicts instance, or they will be silently dropped OR you catch the exception in your command handler and handle it
  • StorageException - an error which couldn't be handled by the framework, e.g. the data store is down or not responding - suggestion for dealing with this is to implement the circuit breaker pattern around the save and checking for this specific exception!