The essence of Strategic Design in DDD is to segregate domain model into well defined areas by business objectives called sub-domains. To some extend, it is Separation of Concerns principle at enterprise level. Having functionally decomposed domain into separate sub-domains with its own distinct bounded contexts, makes software systems much simpler and more responsive to business changes.
Let's imagine typical situation: we are working on legacy project related to banking area within a code base with no clear boundaries between sub-domains, so everything is tightly coupled, architecture leaning toward "Big bull of mud" which is developed and deployed as monolith application (for ex. as one war-file on Tomcat server). However, project is commercially successful and there is a constant need to grow and scale it, so we are challenged by re-factoring and improving architecture.
Making sense of Bounded Context.
We decided to begin re-factoring from deeper analysis of company core concepts: "Accounting" and transfer "Limits". After series of analysis we came up with idea to separate Account and Limit concerns into distinct sub-domains, so each of them would have its own well defined responsibility within its own module:- Accounting sub-domain is responsible for credit/debit operations and keeping balance;
- Limit Management sub-domain provides flexible functionality, which allows end-users to define the limit policy for their accounts;
The obvious question that we must be asking to our self is how interconnected should modules be. After all, the entire model has to work as a whole and there needs to be some interconnections even across isolated modules. Let's have a zoomed look on our model and consider the following use case:
Since Accounting operations depend on Limits (we need to check limits before debit), it looks like "Limits" entity belongs to both sub-domains (please, see Figure 1). Now the challenging question is: How to properly isolate two modules in the case where they are logically coupled?When user wants to debit some amount of money from his account, associated limits must be checked before. If the amount exceeds limit then initiated operation must be interrupted and user should be replied with an appropriate message.

Figure 1. The fragments of "Limit Management" and "Accounting" sub-domains.
The answer lies within business itself! The interesting thing was discovered during discussing "Limit" notion with domain experts. From "Limit Management" point of view, the "Limit" is defined by set of complex rules and policies. For example some of them are:
- daily debit limit entered by user should not be greater than weekly one, otherwise it makes no sense;
- the maximum amount of overdraft limit depends on type of contract, customer profile and monthly incomes etc.;
- authorization rules, for ex. call-center agent could change some limits on user behalf.
At the same time from "Accounting" domain point of view, the "Limit" is just a threshold value which we should not exceed when performing credit/debit operations.
It looks like the word "Limit" is overloaded! Obviously, it has a different semantic meaning depending on the context. The same semantic dissonance happens with notion "Account". Within "Accounting" context it is responsible primary for keeping balance and credit/debit operations, whereas within "Limit Management" context, the "Account" is not credit/debit anymore, but rather it is just an unique identifier (number) limit rules are associated with.
Therefore, when it comes to the concrete modeling we also struggle to puck in all of "Limit" aspects into one entity. Indeed, if concept is overloaded then one single entity will be over-complicated too. Wouldn't be much cleaner if we explicitly defined what the "Limit" does mean for every contexts and implemented two distinct models of "Limit" so every one represents proper vision of target context?
The principle of model segregation is very often used in Software world. Think about Thread! In Java context the Thread model has some major methods such as: start(), interrupt(), join() allowing client code to manage parallel execution, anyway it is very tiny in comparing with what Thread is in context of OS. Indeed, the Thread model is much richer here, it encompasses such things as: execution stack, scheduling, counters, registers etc., it also could be Green or Hyper, or even not thread at all but Process. Could you imagine the level of mess if on Java level we were forced to deal with all those "richness" related to OS level?
How decoupling of two domains is not a big problem anymore. Since for "Accounting" sub-domain the "Limit" is just a threshold value (and here we absolutely don't care about other aspects which are specific to "Limit Management"), we don't have to share one Limit entity between two sub-domains. But what we rely need is to map one concept into another (please, see Figure 2).
Figure 2. The fragments of Context Mappings.
Let's go deeper into code and have a zoomed look on debit use case. It is a typical "check then act" workflow winch goes across sub-domain boundaries:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | package com.ebank.accounting.app; public class AccountingApplication { @Transactional public void debitAccount(AccNumber accNumber, Amount amount){ Limits limits = this.limitService.getLimitsFor(accNumber); Statistics statistics = this.transactionsStatisticService.calculateFor(accNumber); Account account = this.accountRepository.findBy(accNumber); account.debit(amount, limits, statistics); this.accountRepository.save(account); } ... } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | package com.ebank.accounting.domain.model; public class Account { public void debit(Amount amount, Limits limits, Statistics statistics){ if(limits.isDebitAllowed(amount, statistics)){ doDebit(amount); }else{ throw new ExccededDebitLimitException(amount, limits); } } ... } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | package com.ebank.accounting.infrastructure.limits; public class InternalLimitServiceImpl implements LimitService{ private LimitApplication limitApplication; ... public Limits getLimitsFor(AccNumber accNumber){ LimitsData limitsData = limitApplication.getLimitDataFor(accNumber); Limits limits = translate(limitsData); return limits; } ... } |
As you probably may mentioned, the InternalLimitServiceImpl is some kind of adapter, which communicates to "Limit Management" domain and maps foreign concept (LimitsData) into local one (Limits).
Thus, we made a sense of our Bounded Contexts and mapping between them. In his book "Implementing Domain Driven Design", Vaughn Vernon placed Bounded Context into chapter 2, immediately after introduction. Eric Evens said that he would also do that, if he had a chance to rewrite his own book "Domain Driven Design". Both authors agreed - Bounded Context is an corner stone of Strategic Design!
A
Bounded Context is an explicit boundary within which a domain model
exists.... It is often the case that in two explicitly different
models, objects with the same or similar names have different
meanings. When an explicit boundary is placed around each of the two
models individually, the meaning of each concept in each Context is
certain...
Some
projects fall into the trap of attempting to create an all-inclusive
model, one where the goal is to get the entire organization to agree
on concepts
with names that have only one global meaning.
Approaching a modeling effort in this way is a pitfall...
The
best position to take is
to embrace the fact that differences always
exist and apply Bounded Context to separately delineate each domain
model where differences are explicit
and well understood.
--Vaughn Vernon,"Implementing Domain Driven Design". Chapter 2.
Now it's time to scale at strategic level!
Very often along with separation of concerns, different non-functional requirements could be posed for sub-domains, for instance:- Business people might have different level of interest to particular sub-domains, some ones they want evolve and develop, whereas another ones are less susceptible to frequent changes. Usually, "hot areas" are the ones where competitive advantage comes from, and stable ones just supporting first ones. In DDD the first type called Core Domain, the second one - Supporting Domain.
- Some sub-domains are under heavier load that other ones, so different scaling approaches is needed.
- Some complex domains require long learning curve before newcomers became productive, so management prefer to avoid shuffling developers between domains. Team per sub-domain is more cost effective.
- From business point of view "Accounting" sub-domain is pretty stable so once we have implemented credit and debit operations, there are no reasons for frequent changes, whereas with limit concept it's absolutely opposite situation. The limit management is competitive advantage for our company, so business people expect great flexibility there. They are going to concentrate mostly on this area and deliver better and smarter functionality for our users.
- Performance and throughput requirements are different too. On weekends, especially during shopping time, we expect sharp peak of transaction volumes, thus "Accounting" sub-domain will be under very heavy load, whereas "Limit Management" is always under constant moderate load.
- We want to have separate dedicated teams for "Accounting" and "Limit Management", so the first team will concentrate on deep technical stuff related to performance tuning and optimizations, whereas the second team will be more involved into collaborating with business people for improving functionality.
Since we already have segregated sub-domains and established certain context boundaries, splitting monolith shouldn't be a big deal. We need only to re-implement LimitService interface, so in stead of internal in-memory call, it will make REST call to separately deployed "Limit Management" service:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | package com.ebank.accounting.infrastructure.limits; public class ExternalLimitServiceImpl implements LimitService{ private LimitRestClient limitRestClient; ... public DebitLimits getLimitDataFor(AccNumber accNumber){ Response limitsDataResponse = limitRestClient .call(new LimitRequest(accNumber)); Limits lmits = parseLimits(limitsDataResponse); return limits; } ... } |
What we have done here? it is an elementary step toward Microservice architecture:
...the microservice architectural style is an approach to
developing a single application as a suite of small services, each
running in its own process and communicating with lightweight
mechanisms, often an HTTP resource API. These services are built
around business capabilities and independently deployable by fully
automated deployment machinery.
--Martin Fawler, http://martinfowler.com/articles/microservices.html
Autonomy is a crucial ingredient!
Having separated our monolith application into two smaller application (services) and allocated distinct team on each of them, we achieved a great success in solving point 1 and 3 from the requirements list above. Really, now every team could concentrate on their specific issues, without being forced to follow the same agile iteration, release procedures and interfering each others. But the REST call between "Accounting" and "Limit Management" services (which is needed for checking limit during credit/debit operations) becomes a serious disadvantage:
- performance and scalability suffers; one extra remote call takes more time to handle credit/debit request as well as puts extra load on "Limit Management" side.
- reliability significantly decreased; if for some reasons "Limit Management" service is not available then wast majority of "Accounting" functionalities are blocked too;
- harder to test; in order to perform "black box" functional testing of "Accounting" service we need to set up some sort of mocked service which will imitate "Limit Management" functionality;
How could we solve this issue? "Accounting" service needs to pull "Limit Management" service and this is strict dependency! Actually there is one life hack for this sort of issue: "invert pull to push!". Wouldn't it better if instead of pulling "Limit Management" for any single credit/debit request, the "Limit Management" service sends to "Accounting" some sort of notification every time when limits have changed on its side. In turn, the "Accounting" service consumes this notification and update its own "local copy" of limit. So every time when checking limit is needed the "Accounting" service read it from its local database instead of calling remote "Limit Management" service. Actually, in DDD world the mentioned notification is called Domain Event which is published by "Limit Management" and consumed by "Accounting" sub-domains.
A greater degree of autonomy can be achieved when dependent state is already in place in our local system. Some may think of this as a cache of whole dependent objects, but that’s not usually the case when using DDD. Instead we create local domain objects translated from the foreign model, maintaining only the minimal amount of state needed by the local model.
--Vaughn Vernon,"Implementing Domain Driven Design". Chapter 3. Context Maps.
Thus, the "Accounting" domain will have it's own "small" limit model with root entity - "Limits". It stores local copy of limits and it is updated every time when LimitWasChangedEvent is consumed by "Accounting" service:
1 2 3 4 5 6 7 8 9 10 11 12 13 | package com.ebank.accounting.infrastructure.events public class DomainEventListener{ private LimitApplication application; public void handle(LimitWasChangedEvent event){ application.updateLimit(readAccNumber(event), readLimit(event)); } ... } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | package com.ebank.accounting.app.limits; import com.ebank.accounting.domain.model.limits.Limits; import com.ebank.accounting.domain.model.limits.Limit; public class LimitApplication{ ... @Transactional public void updateLimit(AccNumber number, Limit limitRule){ Limits limits = this.limitRepository.findBy(number); limits.addOrUpdate(limitRule); this.limitRepository.save(limits); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | package com.ebank.accounting.app.accounting; import com.ebank.accounting.domain.model.Account; import com.ebank.accounting.domain.model.limits.Limits; public class AccountingApplication { ... @Transactional public void debitAccount(AccNumber accNumber, Amount amount){ Limits limits = this.limitRepository.findBy(accNumber); Statistics statistics = this.transactionsStatisticService.calculateFor(accNumber); Account account = this.accountRepository.findBy(accNumber); account.debit(amount, limits, statistics); this.accountRepository.save(account); } ... } |
Microservices is about distribution by business areas.
The Microservices seems to be promising architecture pattern, it might push scalability and resilience of enterprise to the next level as well as speed up development life cycles. But we always should take on consideration design issues. Since Microservices is about distribution by business responsibilities, the key to success lays on Strategic Design. In other words, on understanding of entire enterprise at higher level and ability to functionally decompose domain into Sub-domains and recognize Bounded Contexts where model is applicable.
However, it worth to mention tactical design issues too. As we already seen, remote calls are slower, much harder to test and are always at risk of failure. Therefore, the services should operate as autonomously as practical. Operations must execute largely independently of surrounding services, which means everyone has to manage eventual consistency. The proper eventual consistency could not be achieved without precise analysis of transaction boundaries, this issue is well known to DDD and addressed by tactical patterns: Aggregate and Domain Event.
Tactics without strategy is the noise before defeat.
--Sun Tzu, "The Art of War" (around 500 B.C)
However, it worth to mention tactical design issues too. As we already seen, remote calls are slower, much harder to test and are always at risk of failure. Therefore, the services should operate as autonomously as practical. Operations must execute largely independently of surrounding services, which means everyone has to manage eventual consistency. The proper eventual consistency could not be achieved without precise analysis of transaction boundaries, this issue is well known to DDD and addressed by tactical patterns: Aggregate and Domain Event.






