Micro Focus is now part of OpenText. Learn more >

You are here

You are here

Domain-driven design: Are your boundaries leaking?

Steven A. Lowe Product Technology Manager, Google

The essential motivation behind domain-driven design is to capture domain knowledge in working code, and then protect that knowledge from corruption by making interface intentions and context boundaries explicit. That means protecting the domain knowledge in your context from contamination and overreaching with other contexts. The result is a system that consists of cooperating but independent, bounded contexts. But what happens when these boundaries are invisible, weak, or leaky, and how can you recognize and fix them? Here's how.

Sometimes it can be hard to see the boundaries, and other times making the boundaries more explicit seems like unnecessary work and complexity. Most of the time, we're just not thinking about the boundaries at all. But explicit boundaries make corruption less likely, which makes the code more stable and informative. Let's look at three leaky boundaries, and how you might reinforce them.

Trust, but validate

Suppose you have a component that must accept an external input. If the external data comes from the user, you have many heuristics and admonitions to not trust it, to validate it unmercifully, to never allow unsanitized user data into your pristine components. But what if the external input comes from another part of the system? Is it OK to trust it, to accept it with open arms, to welcome it regardless of potential risks?

Of course not. Here's an example:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class SalesComponent extends RequestHandler { //We have to override this method to use the communication framework public ResultCode AcceptRequest(String orderRequest) { Json jsonOrderRequest = Json.Decode(orderRequest); //…Do something with jsonOrderRequest… return ResultCode.OK; } //…Many more methods, some public, some not… }

This boundary is leaky because it allows possibly unusable data to enter your context. Note that the method AcceptRequest defines part of the boundary between SalesComponent and the rest of the system. What happens if the orderRequest argument is null?

Here's what: Your component will crash the application. If you're lucky, it will crash on the first statement trying to decode a null String into JSON format. If you're unlucky, the JSON decoder will return an empty JSON structure and your component won't crash until it's deep into the rest of the code, trying to use fields that don't exist.

Either way, your component will crash, and your team will have to fix it. This is the simplest kind of boundary leak; we trusted the input a little too much! This kind of problem is common, but not because programmers forget to apply standard data validations. It happens because they fail to recognize this method as defining the boundary for their context. As soon as you realize that this is a boundary-enforcing method, you know that its input argument is external and therefore must be validated, or at least sanity-checked.

The fix here is simple: Add code to return a not-OK result immediately if orderRequest is null. This moves responsibility for the error to the component that caused it (the one that sent you a null orderRequest) and keeps the application from crashing, or if it does crash, it will do so in the context that caused it instead of in yours!

Create explicit boundaries

Let's look a bit deeper at the first example. In many classes such as this, you have many other methods, not all of which are boundary methods. The intermixing of methods tends to make the context boundary invisible. AcceptRequest looks like just another input method, and its special nature as a context boundary is overlooked; AcceptRequest reveals nothing about the intention of the interface. While we'd rather see something like ApplyOrderRequestToCart, sometimes the environment or framework gives us no choice. We have to make the intention of the domain operation more obvious and the boundary more explicit. In this case, three things are recommended:

  1. Move all the boundary code into a boundary-enforcing class
  2. Keep domain-specific code in the domain classes
  3. Eliminate dependencies on data-carrier structures

For example:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 public class SalesComponentRequestHandler extends RequestHandler { private SalesComponent _salesComponent; public SalesComponentRequestHandler(SalesComponent salesComponent) { _salesComponent = salesComponent; } //We have to override this method to use the communication framework public ResultCode AcceptRequest(String orderRequestString) { //sanity-check incoming external data If (String.isNullOrEmpty(orderRequestString)) { return ResultCode.ERROR; } Json jsonOrderRequest = Json.Decode(orderRequestString); //additional sanity-check if (jsonOrderRequest == null || jsonOrderRequest.isEmpty()) { return ResultCode.ERROR; } //make a domain object from the data-carrier OrderRequest orderRequest = OrderRequestFactory(jsonOrderRequest); //let the domain object do its thing If (_salesComponent.applyOrderRequestToCart(orderRequest)) { return ResultCode.OK; } Return ResultCode.ERROR; } }

It's more obvious that this class defines a boundary for your context, and that's all it does. We've also created an OrderRequestFactory that takes a JSON request and produces a domain object OrderRequest, so that our domain class, SalesComponent, does not have to know anything about JSON. The SalesComponent class can now handle the domain-specific operation using plain-old-objects from our context, and nothing else.

Don't make decisions for other components

Now suppose you've built a hybrid mobile application. Part of the app loads from a web server as a HTML5/JavaScript application, and part of it runs on a mobile device as native wrapper code for the JavaScript component. The native wrapper may need to inform the JavaScript application of things that happened on the device, such as GPS location changes, accelerometer measurements, and so on. To do this, the native framework provides a special "hook" function (say, RunJS) that you can use to execute JavaScript expressions within the JavaScript component.

One day, you're looking at the native wrapper code, and you find the following code fragment:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class NativeWrapperAgent { //This method is called when the user’s GPS location changes public void onGpsLocationChanged(GpsPoint newLocation) { String js = "document.getElementById('gpsView').innerHtml = '" + newLocation.LatLong().toString() + "';"; RunJS(js); //make Javascript app execute js string } //…More stuff here }

It's easy enough to see that the purpose of this method is to inform the web view of changes in GPS location, but the way it does that is somewhat, well, terrifying. It constructs a JavaScript expression and tells the web component to execute it. But isn't that what RunJS does? Isn't that how you have to do it in this environment?

Yes and no. The problem here isn't the mechanism; it's the domain-boundary blurring.

Recognize that this is also a domain-boundary method, but it's an "outgoing" method: It informs the web component, a JavaScript application, of something that happened on the mobile device. The native wrapper is telling the JavaScript component what to do, but it works and it's efficient, so what's so bad about it?

First, the native wrapper has robbed the JavaScript application of the ability to make its own decisions. It should be up to the JavaScript application to decide how to react to a change in the user's GPS location, but here we have the native layer making that decision instead.

Second, the native wrapper has to know far too much about how the JavaScript app works. So even though this is a boundary method, it fails to protect the boundary.

The solution in this case is to push all the foreign JavaScript knowledge out of the native wrapper's context, by sending the JavaScript app simple notifications in a neutral format. The JavaScript app can decide how and when to react to them. For example:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class NativeWrapperAgent { //This method is called when the user’s GPS location changes public void onGpsLocationChanged(GpsPoint newLocation) { String jsonGpsPoint = JsonEncoder.encode(newLocation); String js = "handleEvent('" + jsonGpsPoint.toString() + "');"; RunJS(js); //notify Javascript app of new event } //…More stuff here }

Now the native wrapper only has to know the name of a single event-handling function in the JavaScript app, instead of its entire internal structure, and the JavaScript app decides what to do with each event. This strengthens the boundary by minimizing the amount of foreign knowledge (JavaScript code) required and using a neutral data carrier to transmit the event notifications.

Good fences make good neighbors

Our examples range from trivial to fairly complex, but they all have one thing in common: inadequate boundaries. So look for domain boundaries in your code, especially those protecting you from external domains. Validate incoming objects before allowing them to roam free within your context. Encapsulate boundary methods in dedicated classes and objects; do not intermix domain model operations and boundary methods. Minimize external knowledge and interface complexity.

These habits will make both your components and external components more independent, more stable, and much easier to understand.

Keep learning

Read more articles about: App Dev & TestingApp Dev