In his celebrated
book on Domain Driven Design, Eric Evans mentions that one of the biggest challenges of maintaining the integrity of a domain model is managing the lifecycle of a domain object. He states
But other objects have longer lives, not all of which are spent in active memory. They have complex interdependencies with other objects. They go through changes of state to which invariants apply. Managing these objects presents challenges that can easily derail an attempt at MODEL-DRIVEN DESIGN.
Two points immediately stand out from the above, when we think of modeling a system based on principles of DDD :
- Domain object lifecycles need to be managed
- Lifecycle management needs to be decoupled from mainstream business logic
Both of the above issues can be addressed using an IoC container like
Spring. And the new custom scopes of
Spring 2.0 give us the real steroid towards
declarative lifecycle management of domain objects.
Custom Scopes in Spring 2.0Till 2.0, Spring provided only two levels of granularity at which you could declare your beans -
- Singleton, which scopes a single bean definition to a single object instance per Spring IoC container and
- Prototype, which scopes a single bean definition to any number of object instances. The prototype scope results in creation of a new bean instance every time a request for that specific bean is made.
Hence any other intermediate granularity of bean lifecycles had to be managed explicitly by the application itself. Spring 2.0 comes with three additional levels of bean scopes out of the box, as well as the framework to create your custom scope that suits your application needs. The ones that come out of the box are
request,
session and
global session, which are described in detail in the Spring Reference documentation.
In this blog post, I will elaborate how the domain model can be enriched by defining beans at application defined custom scopes. I will end the post with an example of how declaring the scope as configuration, results in declarative lifecycle management of a domain object, irrespective of the layer to which the object belongs.
Scope your Domain Objects at Business Level GranularityPrior to 2.0, non-singleton beans could only be defined with a lifecycle that creates a new instance on every access - aka the
prototype scope. For beans not created by Spring, there used to be techniques like
Field Level Injection and Service Location Strategy which achieves the same effect as prototype beans.
Let us consider a real life example from the financial domain, specifically, a back office system for Capital Market Trading and Settlement. We have an example of a
BasketTrade
, which is, essentially a collection of
Trade
s matching some criteria and need to be processed together. No surprises here, we model a
BasketTrade
as :
public class BasketTrade {
private List<Trade> trades = new ArrayList<Trade>();
// ..
// ..
}
The point to note is that the abstraction
BasketTrade
is just a virtual container for the collection of trades and has been created for the convenience of atomic processing of the underlying
Trade
objects. It is typically the root of an Aggregate, as Eric defines in his DDD book, which ceases to exist as soon as the processing is complete, e.g. all component trades are committed to the Repository.
In other words, we can define a custom scope (say,
basket-scope), which defines the lifecycle of a
BasketTrade
bean. Typically, the application provides a
BasketingService
, which can be modeled as a Singleton, and can contain a
BasketTrade
as a scoped bean within it, injected by the IoC container.
public class BasketingService {
private BasketTrade basket;
public void addToBasket(Trade trade) {
getBasket().add(trade);
}
public void setBasket(BasketTrade basket) {
this.basket = basket;
}
public BasketTrade getBasket() {
return basket;
}
}
We have two collaborating beans with different lifecycles, which can be wired up with the custom scope definitions as part of the configuration. And Spring 2.0 offers
ScopedProxyFactoryBean
for this purpose, which offers convenient proxy factory bean for scoped objects. Here we have the xml, which wires the domain objects with custom life cycles :
<bean class="org.springframework.beans.factory.config.CustomScopeConfigurer">
<property name="scopes">
<map>
<entry key="basket"><bean class="org.dg.biz.trd.BasketScope"/></entry>
</map>
</property>
</bean>
<bean id="basketTradeTarget"
class="org.dg.biz.trd.BasketTrade" scope="basket" lazy-init="true">
</bean>
<bean id="basketTradeProxy"
class="org.springframework.aop.scope.ScopedProxyFactoryBean">
<property name="targetBeanName">
<value>basketTradeTarget</value>
</property>
</bean>
<bean id="basketingService" class="org.dg.biz.trd.BasketingService">
<property name="basket" ref="basketTradeProxy"/>
</bean>
Defining the BasketScopeThe following is a very naive implementation of the custom basket scope. This is just for demonstrating the power of custom scopes in controlling the lifecycles of domain objects.
public class BasketScope implements Scope {
private static final Map scope = new ConcurrentHashMap();
public String getConversationId() {
return null;
}
public Object get(String name, ObjectFactory objectFactory) {
Object obj = scope.get(name);
if (obj == null) {
obj = objectFactory.getObject();
scope.put(name, obj);
}
return obj;
}
public Object remove(String name) {
return scope.remove(name);
}
public void registerDestructionCallback(String string, Runnable runnable) {
// register any custom callback
}
}
Removing Objects with Custom ScopeThis is an area which is not very clear from Spring Reference documentation. No problem, the helpful Spring community was prompt enough to give enough support to clarify all my confusions (see this
thread).
For the out-of-the-box implementations of request and session scopes, the lifetime of the scoped bean ends automatically with the end of the request or session - and one can implement
HttpSessionBindingListener
to plug in custom destruction callback. Have a look at the implementation of
DestructionCallbackBindingListener
in class
org.springframework.web.context.request.ServletRequestAttributes
:
private static class DestructionCallbackBindingListener
implements HttpSessionBindingListener {
private final Runnable destructionCallback;
public DestructionCallbackBindingListener(Runnable destructionCallback) {
this.destructionCallback = destructionCallback;
}
public void valueBound(HttpSessionBindingEvent event) {
}
public void valueUnbound(HttpSessionBindingEvent event) {
this.destructionCallback.run();
}
}
For a custom scope, the simplest way will be to invoke the object removal manually in the workflow. For the above example with
BasketTrade
, the destruction code looks like :
ScopedObject so = (ScopedObject) basketingService.getBasket();
so.removeFromScope();
Note that proxies returned by ScopedProxyFactoryBean
implement the ScopedObject
interface, which allows removing the corresponding object from the scope, seamlessly creating a new instance in the scope on next access.And now on to a neat trick. We can encapsulate the invocation of the destruction callback into a
Seam-style annotation marking the end of the conversation scope. In the above code for
BasketingService
, suppose we would like to end the scope of the basket after a commit to the database of all constituent trades - we have a method
commit()
, which after database commit will mark the end of the lifetime of the current basket. We mark this declaratively using the
@End
annotation.
public class BasketingService {
// as above
@End
public BasketTrade commit() {
// database commit logic
return basket;
}
}
Finally the implementation of
@End
using the usual Spring AOP magic ..
@Aspect
public class DestroyScope {
@AfterReturning(
pointcut="@annotation(org.dg.biz.trd.End)",
returning="retVal")
public void doDestroy(Object retVal) {
ScopedObject so = (ScopedObject) retVal;
so.removeFromScope();
}
}
and the corresponding entry in configuration xml :
<aop:aspectj-autoproxy/>
<bean id="destroyAspect" class="org.dg.biz.trd.DestroyScope"></bean>
ConclusionOne of the great benefits of custom scopes in Spring 2.0 is the fact that it allows declarative lifecycle management of domain objects without the service location api intruding your business logic code. In the above example, the two wired beans
BasketingService
and
BasketTrade
have different lifecycles - yet the collaborating code and the associated business logic is completely oblivious about this difference. The declarative
@End
annotation, along with the Spring AOP magic, works behind the doors to automatically fetch a new instance of
BasketTrade
when the user asks for the next access.