Applying Domain-Driven Design with Salesforce

Denis Krizanovic
Salesforce Architects
12 min readAug 10, 2021

--

Pipes on a wall form geometric shapes.

Domain Driven Design (DDD) is a software development approach in which implementation artifacts are tightly connected to an evolving model of business domain concepts. It contains numerous patterns, two of which (bounded contexts and shared kernels) are explored in this post. In a Salesforce context, DDD involves having multiple disparate applications — each with its own interpretation of key entities such as Case or Contact — in a single organization, something that Salesforce was not designed to do. In an ideal world, I would never put applications from multiple domains in one organization, but in practice I’ve found myself doing exactly that. I know of others that have found themselves in this same situation, and due to poor architectural choices, they’ve had to pay a steep price: longer time-to-value, endless regression testing, and recursive governance meetings.

This post covers how to set your organization up from the outset to manage applications from different domains. It describes an approach for using DDD with Salesforce without finding yourself battling governance and technical debt problems years after your organization has been in production. This approach has a number of potential advantages — leaner governance, greater velocity, team autonomy, and package-based development — but it also requires some significant trade-offs. As an architect, you’ll need to decide if the trade-offs are worth the potential benefits in your context.

In a sense, DDD requires you to look at Salesforce “Through the Looking Glass”, and see it in a new way — as a platform for many diverse apps.

Bounded contexts and shared kernels

Domain-Driven Design helps us view the Salesforce platform from a different perspective. In particular, the bounded context and shared kernel patterns of DDD enable us to recognize where different domains cross over and share domain objects and where there is no overlap.

Bounded contexts are a DDD technique that enables us to understand how the same concept can be interpreted differently in different domains. The legal team, for example, may have a different view of the mandatory attributes of an Account object than the sales team in the same company.

The DDD notion of shared kernels facilitates an understanding of shared CRM objects, such as Account, Contact, Case, and so on. The shared kernel is that part of the domain model that is shared between bounded contexts. As a result, any changes to the shared kernel require consultation with all stakeholders and more rigorous governance processes.

The following diagram illustrates a situation where four disparate applications are operating within a single organization. They share the same CRM kernel but have different perspectives on the objects within it.

Diagram illustrating four disparate applications operating within a single Salesforce organization.

Why would you adopt a DDD-oriented approach with Salesforce?

Salesforce is often purchased to initially enable a single business function, for example, the service center or the sales team. For the sales team, each Contact represents someone who bought a product, whereas for the service center the Contact is someone who needs service on the product. The concepts are somewhat related. Custom applications built within this model are satellites to this primary domain, either sales or service — but it doesn’t always stay that way. As Salesforce increases its footprint in the organization, other teams see its value and want in.

What happens when the legal department sees the potential of Salesforce and wants to build a couple of applications for their use? In this case, the Contacts are other lawyers who may never buy your product and will only interact with your Legal team. Or, what if the marketing team wants to build a portal that advertising agencies can use to bid for various campaigns. Perhaps the data scientists want some specific tracking app for their work and they want to build this single-purpose app in Salesforce as citizen developers. In this situation, the business domains are vastly different and have little, if any, overlap.

Many applications, no matter the domain, have an object in them representing a person. This person will typically belong to an organization and will make an inquiry or require some work to be performed. That said, it’s important to think carefully about whether every app you create truly requires Account, Contact, and Case objects.

How do you effectively manage many diverse groups/business units in a single Salesforce organization, particularly if they have their own interpretation of the word “Case” or “Contact”? What if they want to move at different speeds and do things that break the norms of traditional CRM use cases, such as not using Case reflexively every time there is a “Case” to manage?

Imagine a scenario in which you had three applications from the different domains, all executing in the same Salesforce organization. They have additional unique attributes on Account/Contact/Case, which you manage through record types. Not too bad.

What if you had ten applications, with ten different teams working on them? It doesn’t matter too much if these ten are being developed in parallel or that some are in run-mode only. Once an application is a resident on the platform, it is an enduring stakeholder and consequently a parallel stream.

Is it possible to reason correctly about what will happen with the criteria-based sharing rules when an innocuous-sounding user story is developed that has downstream effects on the criteria fields? What if there are unexpected implications on one of the triggers when a shared picklist is changed?

Of course, it is possible to address these issues with better governance, stricter controls, more up-front design, or larger batteries of automated tests. You can make it work for a while, but you can see it’s getting harder to reap the benefits of Salesforce as a rapid application platform with low-code, democratic tools that allow anybody to build any app they need.

What if you had 30 apps? 50?

What a DDD-oriented approach enables

Effective DDD leads to cycle time reductions for moving from ideas to production. As you likely know from your DevOps research, cycle time is the leading indicator for maximizing business value, decreasing cost-to-serve, and driving quality improvements. Each of the following aspects of the DDD-oriented approach described here helps free the delivery team to pursue its goals more effectively without being coupled to the entire organization and its history.

The structure of bounded contexts provides the freedom to go fast, and not break things.

Leaner governance

Governance is the system of rules, relationships, and processes by which decisions are made. Simply put, it defines who gets to make what decision and when. Governance in a multi-domain organization exist on a spectrum that ranges from almost none to the other extreme at which every decision is tightly reviewed, or made, by a global group of stakeholders. Neither of these options is ideal for an organization with many disparate apps.

Defining the shared kernel and the bounds of each application allows for tighter decision rights in the shared kernel and a much more delegated approach within the application’s own scope. The shared kernel enables each team to make localized and application-specific decisions quickly to best meet its own business needs. Having a strong set of guardrails/guidelines on when and how to use the shared kernel is key. It is equally important to have a good mechanism to track decisions and modifications to these standards, as there will inevitably be.

Defining what is in the shared kernel, and what is out, enables the architecture team to focus on the aspects of the platform that affect the whole platform. Consequently, it reduces the cycle-time of stories, since fewer decisions require the involvement of all stakeholders on the platform.

Team structure

Orienting your organization around domains enables teams to operate mostly independently. Low coupling in the architecture allows for low coupling in the team structure. Low coupling leads to lower friction as well as faster decision-making and implementation, which in turn reduces the cycle-time of delivery.

Team skills growth is also enabled. Clear accountability for design decisions can provide the space for less experienced developers to practice their design skills, knowing that their blast area is constrained. The teams take on more design and have the freedom to see how their design decisions play out in production. A generative culture is supported when delegated decision rights promote empowerment.

Speed of development

Teams that have a clear understanding of the boundaries of their decision rights don’t need to be concerned with all the other applications in the organization. As a result, their cognitive load is reduced, they are not paralyzed with uncertainty, and their overall speed of development increases.

You will find that decision guides are important for helping the teams working in different domains make the right technology choices. You will likely need to extend these guides with context-specific additions, but they serve as an excellent starting point to drive alignment across your teams and to facilitate speed without breaking things.

Further, you’ll find that regression testing is shorter and simpler. Bounded contexts allow the new software to be processed by the software development delivery system faster.

Citizen developers

Just as DDD enables development teams to play in their own contexts, it enables citizen developers to do the same. Delineating the area in which different apps and teams can play has the benefit of ring-fencing citizen developers. As a result, they can express the needs of their business requirements freely. Of course, you’ll want to establish enablement and governance structures to enable this fully.

Package-based development

Package-based development is currently a kind of North Star in the Salesforce ecosystem. By adopting a bounded context and shared kernel approach you can lay the foundations of your organization to make the transition to packaged-based development a lot easier, when you are ready. Shifting the cultural mindset to thinking package-first is a larger problem than adopting the tooling necessary to accomplish it at a technical level. When you delineate your organization with bounded contexts, you get a head start on this essential cultural change.

As an additional benefit, packages further enforce the delineation with physical structures, making it harder to cross the boundaries.

Be sure to read 5 Anti-Patterns in Package Dependency Design and How to Avoid Them for details on how to design and build loosely coupled unlocked packages using the dependency injection design pattern, a very useful technique for avoiding liabilities associated with package dependency.

What a DDD-oriented approach requires

DDD with Salesforce requires you to make certain tradeoffs. Depending on your specific teams and situation these may be a heavy lift and may in fact outweigh the advantages of this approach.

Strong namespacing

A core requirement of bounded context delineation is the ability to clearly namespace every artifact on the platform. It is critical you understand where every class, field, rule belongs, otherwise you end up in an “unhappy soup”. Unlike Java or .NET, Salesforce does not have a robust namespace or folder approach. So, you need to invent one. Managed packages do have their own model, but it does not translate to internally developed solutions. Having these namespaces lets you more easily reason about your organization and make assertions about expected outcomes with greater confidence.

The most common approach is to use a common prefix separated with an underscore. For example: “ns1_”. The trick is to make sure every artifact has this — from fields, to flows, to sharing rules, to permission sets and email templates.

Code-based approach

Code has the benefit of being able to express your abstractions in precisely the right manner and to communicate intention through structure and relationships. The shared kernel will be doing a lot of heavy lifting for many different contexts, so it needs the benefit of the best tools with the best design patterns. As a result, you’ll need a code-based approach.

Don’t be surprised if you find yourself pondering whether you should rewrite some standard object functionality on a custom object just to pull pressure off of the shared kernel. It might make sense. For example, having the ability to email from and to any object, because you want to stay off the Case object for a particular bounded context.

You may also find yourself rebuilding a Salesforce feature that assumes some underlying feature that you can’t use. Again, it might make sense, however uncomfortable it makes you feel. For example, roles can be challenging with shared kernel objects, as the rollups introduce confusion higher up the hierarchy. So, you may need to create your own hierarchy that doesn’t always share Contact records with those above. Decisions like this need careful, well-documented decision-making.

You may find yourself in the Ri stage of the Shuhari. You’ll know when the context and the trade-offs suggest breaking the rules, and as an architect, you have weighed the consequences, with ample documentary proof of your decision-making process.

Not everything will require code, the further away you from the shared kernel, the less you need code to overcome the inherent coupling and therefore the more straightforward your declarative solutions can be. So minimizing an application’s dependence on the kernel is critical.

Adding fields to objects in the shared kernel

You might think that with this great new prefix on every field, you can just add fields to standard objects in the shared kernel anytime you like. You can use record types to spin page layouts for different apps. Yes, you can do that, but are you overloading those shared kernel objects? Are you increasing design debt that will slowly undermine your cycle-time with each new app?

Every change you introduce into the shared kernel has ramifications in terms of regression testing and review.

To optimize for speed and modularity, it’s best if you put your specific app’s fields in a child object to the kernel. In this case, your new custom application will have an extension object, for example, ns1_Case_myCustomApp__c. This allows you to manage the lifecycle of your object independently of the objects in the kernel and distances you from any of its issues.

Of course, there are trade-offs to this approach. The user experience is compromised as end users have two objects to edit instead of one. There are solutions to this user-experience issue with different Lightning components, screen flows, or even a custom LWC. Listview and reports now need to cover two objects, where they are designed to look at only one. Declarative development for the citizen developer becomes more difficult, as they have to deal with extension objects.

The trade-offs in this area are significant. You want to be sure you think through all the implications and that both you and your business are willing to accept them.

Authorization spectrum

The further you find yourself away from the kernel, the simpler your sharing model can be. The shared kernel will be providing records for many contexts, and consequently will need to be flexible for all manner of contexts.

Extension objects, as discussed above, may require some form of Apex managed sharing, as you will be effectively sharing records in two objects with a single update.

An application-specific object (specific to only one user group) can be managed with CRED (Create, Read, Edit, Delete) rules in permission sets since users from only one domain will have access to the object. Applying Best Practices for Permission Sets is key to ensuring dependencies between apps is maintained.

Automated technical governance

Mandating namespace prefixes on everything, and then dealing with the inevitable exceptions, is not work fit for a human. Static analysis of configuration is required to keep all the bounded contexts from bleeding into each other and turning your organization into unmanageable spaghetti.

You’ll need strong continuous integration. The earlier you discover where a bounded context has been broken, or where a model has bled into another, the easier it is to fix. This becomes more important as apps become run-only, and all the developers leave or move on to other apps. The best way to discover such issues is via automation. A future post will detail how you can develop your own static analysis of configuration via a series of simple scripts.

Automated technical governance must be part of a robust release management process that encompasses these scripts and includes collaborative code and config reviews to coach developers and reinforce this bounded context mindset.

Adopting objects from the shared kernel with care

By adopting an object from the shared kernel, you are coupling yourself to every other application that also adopts this object. You will need to weigh the benefits. For example, if you are dealing with work items for a legal team, consider if they really are “Cases” from a Salesforce perspective? Do they actually require all the additional functionality that comes with Case? Or, in this context, is it better to just use a custom object, and remove this dependency?

I deliberately left this topic for last as it may be the most contentious. There are indeed many benefits to standard objects in terms of existing capabilities, evolving capabilities, AppExchange package expectations, and more. And this is balanced by the discussion above on “Adding fields to objects in the shared kernel”.

Back through the looking glass

By looking at the Salesforce organization from the perspective of multiple bounded contexts, you get a clearer view of the benefits and trade-offs of hosting numerous apps in a single organization. Perhaps you’ve seen that this plumbing needs to be put in place before everyone starts creating change sets. Putting the genie back into the bottle is hard and expensive. It’s better to structure your organization in this way to begin with, if your context is suggesting it.

The technical necessities of this approach are non-trivial, and the cultural practices and technical constructs that must surround your software development delivery system take time and patience to develop. Adopting the DDD practices outlined in this post, however, can provide benefits in terms of speed of delivery and simplicity of reasoning so your company can make the most of its use of Salesforce.

--

--