Bridging Business and Code: Addressing the Gap Between Domain Experts and Developers with Domain-Driven Design and the Pitfalls of CRUD Terminology

Mohamed Hendawy
11 min readJan 19, 2024

--

One of the most widely spread problems in enterprise-level development is the way programmers approach data modification. all operations in an application, fundamentally fall into one of the four categories: create, read, update, and delete; CRUD for short. And it’s true. Technically, everything we do is either create, read, update, or delete something, but nevertheless, it’s never a good idea to organize your application along these lines, except for the simplest cases. In fact, such an organization can have a devastating effect on your system, and not only in terms of its maintainability. It damages the user experience, too, We will call this approach to code design CRUD-based interface, and the overarching mentality, CRUD-based thinking. So, what’s the problem with it, exactly? There are three of them. The first one is uncontrolled growth of complexity capturing in a single method all operations that somehow mutate an object, leads to enormous expansion of that method. At some point the complexity becomes unbearable. This, in turn, entails lots of bugs when modifying something in the code base and failures to meet project deadlines and quality standards. And this point comes much sooner than you might realize. The second problem is the disconnect between how the domain experts view the system and how it is implemented. Your domain experts don’t speak in CRUD terms, and if they do, it’s because you trained them to, not because it’s natural for them.

Think about it. When you go to college for the first time, does the administrator tell you that she will go ahead and create you in their system? No. She will tell you she is going to register you, not create, and if you decide to take a Calculus course, does she describe it as updating you? Absolutely not!. She will enroll you in this course, not update.

Another way to describe this problem is as lack of ubiquitous language. Ubiquitous language is one of the three pillars of Domain-Driven Design. It’s essential for any enterprise-level development to set up a proper communication channel between the programmers and domain experts, and the best way to do so is to impose a single, unified ubiquitous language within a bounded context, and then have both domain experts and programmers speak this language. And not only speak, but use in it code.

The third problem is damaging the user experience. You see, the problem of the CRUD-based thinking never stays within the boundaries of your application code. It also affects the UI. The CRUD-based thinking spills over from the code to the user interface, and the same issues that plague the code itself infect the user experience, too.

why is CRUD-based interface so widely spread? It’s because of the programmers’ desire to unify everything. Just look at the premise of object-oriented programming, which states that everything is an object, and so why not take the same approach and define every operation in terms of CRUD, too? From the perspective of us as programmers, this leads to a nice-looking and clean system, where all of the APIs have one narrowly defined purpose. Who wouldn’t like it? But of course, this doesn’t lead anywhere. What you need to do instead is talk to domain experts, discover proper terms, and adjust your thinking accordingly.

Another Example:
The Problem with CRUD Terminology

Domain Expert: “When a customer places an order, we need to calculate the applicable discounts, verify inventory, and update the order status based on fulfillment.”

Developers, while aiming to fulfill these requirements, might express the solution in CRUD terms:

public class OrderService
{
public void CreateOrder(Order order)
{
// Business logic for creating an order
}

public void UpdateOrderStatus(Order order, OrderStatus newStatus)
{
// Business logic for updating order status
}

public void ApplyDiscount(Order order, Discount discount)
{
// Business logic for applying discounts
}

public void VerifyInventory(Order order)
{
// Business logic for verifying inventory
}
}

While technically correct, this representation lacks the expressiveness needed to truly capture the richness and complexity of the business processes discussed by the domain expert.

So, how to fix this issue? The opposite of CRUD-based interface is task-based interface and Domain-Driven Design (DDD) as the Solution

Navigating the Divide with Domain-Driven Design (DDD)

1. Ubiquitous Language: Bridging the Vocabulary Gap

Concept: Ubiquitous Language, a fundamental principle of DDD, advocates for the creation of a shared vocabulary between domain experts and developers. This shared language ensures a consistent understanding of terms across the entire project.

Example: In an e-commerce system, terms like “Order,” “Customer,” and “Inventory” have specific meanings consistently used across conversations, code, and documentation.

public class Order
{
public void PlaceOrder(Customer customer, List<OrderItem> items)
{
// Business logic for placing an order
}
}

Scenario: E-Commerce Domain

In the context of an e-commerce domain, the term “Cart” could have different meanings for customers, administrators, and developers. Embracing Ubiquitous Language ensures a consistent understanding across all stakeholders.

public class ShoppingCart
{
public void AddItem(Product product, int quantity)
{
// Business logic for adding items to the shopping cart
}

public void RemoveItem(Product product)
{
// Business logic for removing items from the shopping cart
}

public void Checkout(Customer customer)
{
// Business logic for processing the checkout
}
}

Here, “ShoppingCart” is a term commonly used by customers, administrators, and developers alike, fostering a shared understanding of the e-commerce process.

Scenario: Healthcare Domain

Consider the term “Patient Admission” in a healthcare system. It might involve different steps and processes for healthcare providers, administrative staff, and software developers. Ubiquitous Language ensures a unified interpretation.

public class AdmissionService
{
public void AdmitPatient(Patient patient, AdmissionDetails admissionDetails)
{
// Business logic for admitting a patient
}

public void DischargePatient(Patient patient)
{
// Business logic for discharging a patient
}

public PatientRecord GetPatientRecord(Patient patient)
{
// Business logic for retrieving a patient's record
}
}

In this scenario, “AdmissionService” encapsulates the domain knowledge related to patient admission, providing a common ground for communication.

Certainly! Let’s delve deeper into the concept of Ubiquitous Language with additional examples:

1. Ubiquitous Language: Bridging the Vocabulary Gap

Scenario: E-Commerce Domain

In the context of an e-commerce domain, the term “Cart” could have different meanings for customers, administrators, and developers. Embracing Ubiquitous Language ensures a consistent understanding across all stakeholders.

public class ShoppingCart
{
public void AddItem(Product product, int quantity)
{
// Business logic for adding items to the shopping cart
}

public void RemoveItem(Product product)
{
// Business logic for removing items from the shopping cart
}

public void Checkout(Customer customer)
{
// Business logic for processing the checkout
}
}

Here, “ShoppingCart” is a term commonly used by customers, administrators, and developers alike, fostering a shared understanding of the e-commerce process.

Scenario: Healthcare Domain

Consider the term “Patient Admission” in a healthcare system. It might involve different steps and processes for healthcare providers, administrative staff, and software developers. Ubiquitous Language ensures a unified interpretation.

public class AdmissionService
{
public void AdmitPatient(Patient patient, AdmissionDetails admissionDetails)
{
// Business logic for admitting a patient
}
public void DischargePatient(Patient patient)
{
// Business logic for discharging a patient
}
public PatientRecord GetPatientRecord(Patient patient)
{
// Business logic for retrieving a patient's record
}
}

In this scenario, “AdmissionService” encapsulates the domain knowledge related to patient admission, providing a common ground for communication.

Scenario: Real Estate Domain

In the real estate domain, the term “Listing” may refer to different aspects such as property listings, agent listings, or listing agreements. Ubiquitous Language ensures clarity.

public class ListingService
{
public void CreatePropertyListing(Property property, Agent agent)
{
// Business logic for creating a property listing
}
public void UpdateAgentListing(Agent agent, ListingDetails listingDetails)
{
// Business logic for updating an agent's listing
}
public ListingDetails GetListingDetails(Listing listing)
{
// Business logic for retrieving listing details
}
}

Here, “ListingService” encapsulates the domain understanding of different stakeholders involved in real estate operations.

Benefits of Ubiquitous Language:

  1. Clarity in Communication:
  • Ubiquitous Language ensures that terms used in conversations, code, and documentation have the same meaning, reducing misunderstandings.

2. Consistency Across Stakeholders:

  • Whether it’s a customer, domain expert, or developer, everyone involved in the project can share a common vocabulary, fostering a collaborative environment.

3. Aligned Code Representation:

  • The code becomes a reflection of the business domain, making it easier to maintain and evolve as the domain requirements change.

2. Bounded Context: Defining Clear Boundaries

Concept:

In Domain-Driven Design, a Bounded Context is a critical concept that allows for the creation of explicit boundaries where specific terms and concepts hold distinct meanings. Different Bounded Contexts may interpret the same terms differently, preventing ambiguity.

Scenario: Banking Domain

Consider the term “Transaction.” In the context of online banking, a “Transaction” may represent a fund transfer. In the context of fraud detection, it might signify a suspicious activity alert. Bounded Contexts provide clear separation.

namespace OnlineBankingContext
{
public class TransactionService
{
public void TransferFunds(Account sourceAccount, Account destinationAccount, decimal amount)
{
// Business logic for transferring funds
}
}
}

namespace FraudDetectionContext
{
public class TransactionMonitoringService
{
public void MonitorTransaction(Transaction transaction)
{
// Business logic for monitoring transactions for fraud
}
}
}

Here, the Transaction entity has different meanings within distinct Bounded Contexts, preventing confusion.

Scenario: E-Commerce Domain

In the e-commerce domain, the term “Product” might have different interpretations. In the context of inventory management, it could represent a physical item. In the context of recommendation engines, it might be a digital offering. Bounded Contexts maintain clarity.

namespace InventoryContext
{
public class ProductInventoryService
{
public void UpdateStock(Product product, int quantity)
{
// Business logic for updating product stock
}
}
}

namespace RecommendationContext
{
public class ProductRecommendationService
{
public List<Product> GetRecommendedProducts(Customer customer)
{
// Business logic for recommending products based on customer behavior
return new List<Product>();
}
}
}

Here, within separate Bounded Contexts, “Product” is defined with precision, avoiding ambiguity.

Benefits of Bounded Contexts:

Clear Separation of Concerns:

  • Bounded Contexts allow for the isolation of specific business concerns, making it easier to comprehend and manage complex domains.

Avoidance of Ambiguity:

  • Different meanings of terms within distinct Bounded Contexts eliminate confusion and ambiguity across the project.

Focused Development:

  • Developers can focus on specific contexts without being overwhelmed by the entire complexity of the domain.

3. Aggregate

Concept:

Aggregates in Domain-Driven Design (DDD) are a way to group related entities and value objects into a cohesive unit, ensuring transactional consistency. The aggregate root is the primary entry point for interactions within the aggregate.

Example 1: E-commerce System

Consider an e-commerce system with a “Order” aggregate. The aggregate includes the “Order” entity, “Product” entities representing items in the order, and a value object, “ShippingAddress.”

public class Order
{
private List<Product> products = new List<Product>();
private ShippingAddress shippingAddress;

public void AddProduct(Product product, int quantity)
{
// Business logic for adding a product to the order
}

public void ShipOrder()
{
// Business logic for processing shipping of the order
}
}

Here, the “Order” aggregate ensures that operations like adding products or shipping the order are performed through the aggregate root, maintaining consistency in the order’s state.

Example 2: Project Management System

In a project management system, a “Project” aggregate might include entities such as “Task,” “TeamMember,” and a value object, “ProjectDetails.”

public class Project
{
private List<Task> tasks = new List<Task>();
private List<TeamMember> teamMembers = new List<TeamMember>();
private ProjectDetails projectDetails;

public void AddTask(Task task)
{
// Business logic for adding a task to the project
}

public void AssignTeamMember(TeamMember teamMember)
{
// Business logic for assigning a team member to the project
}
}

In this example, the “Project” aggregate ensures that actions like adding tasks or assigning team members are controlled through the aggregate root, preserving the integrity of the project’s data.

Example 3: Health Record System

Imagine a health record system with a “PatientRecord” aggregate. It may include entities like “MedicalHistory,” “Prescription,” and a value object, “PatientDetails.”

public class PatientRecord
{
private List<MedicalHistory> medicalHistory = new List<MedicalHistory>();
private List<Prescription> prescriptions = new List<Prescription>();
private PatientDetails patientDetails;

public void AddMedicalHistory(MedicalHistory history)
{
// Business logic for adding medical history to the patient record
}

public void AddPrescription(Prescription prescription)
{
// Business logic for adding a prescription to the patient record
}
}

In this case, the “PatientRecord” aggregate ensures that medical history entries and prescriptions are managed through the aggregate root, providing a consistent view of the patient’s health data.

Aggregates simplify the design and maintenance of complex systems by grouping related components together. They define transactional boundaries, ensuring that changes within the aggregate are treated as a single unit.

4. Repository: Abstracting Data Access

Concept:

A Repository provides a mechanism to access and store aggregates. It abstracts away the details of how objects are persisted or retrieved, allowing for flexibility in data access.

Why It Matters:

In a DDD context, the Repository pattern plays a crucial role in managing the persistence of aggregates. It abstracts away the complexities of data access, allowing the domain layer to remain focused on business logic without being tightly coupled to specific data storage implementations.

Example:

In a banking application, a “CustomerRepository” might have methods like “GetCustomerById” and “SaveCustomer” to interact with the underlying data store.

public class CustomerRepository
{
public Customer GetCustomerById(int customerId)
{
// Logic to retrieve customer from the data store
}

public void SaveCustomer(Customer customer)
{
// Logic to persist customer to the data store
}
}

Example:

Consider a scenario where you have an “Employee” aggregate in an HR system. The repository pattern provides a clean interface for retrieving and storing employee aggregates without exposing the underlying data storage details.

public class EmployeeRepository
{
public Employee GetEmployeeById(int employeeId)
{
// Logic to retrieve employee from the data store
}

public void SaveEmployee(Employee employee)
{
// Logic to persist employee to the data store
}
}

By using a repository, the rest of the application can interact with employees without concerning itself with whether the data is stored in a relational database, a document store, or any other storage mechanism.

5. Services: Encapsulating Business Logic

Concept:

Services encapsulate domain logic that doesn’t naturally fit within the concept of an entity or a value object. They represent actions or operations.

Why It Matters:

Services in DDD encapsulate domain logic that doesn’t naturally fit within the concept of an entity or a value object. They represent actions or operations that involve multiple entities or require complex computations.

Example:

Imagine a “BillingService” in a healthcare system. Calculating a patient’s bill involves intricate business rules, including insurance calculations, discount application, and updates to financial records. A service provides a clean and cohesive way to handle this complex logic.

public class BillingService
{
public void CalculateBill(Patient patient)
{
// Business logic for calculating and generating the patient's bill
}
}

Services promote a more modular and maintainable codebase by centralizing domain logic that doesn’t naturally belong to a specific entity.

6. Domain Events: Reacting to Significant Changes

Concept:

Domain Events represent significant state changes or occurrences within the domain. They allow different parts of the system to react to those changes.

Why It Matters:

Domain Events allow different parts of the system to react to significant state changes or occurrences within the domain. They facilitate loose coupling between different components, enabling a more responsive and event-driven architecture.

Example:

In a logistics system, an “OrderShipped” event might be triggered when an order is successfully shipped, allowing other modules to update inventory and notify customers.

public class OrderShippedEvent
{
public int OrderId { get; private set; }
public DateTime ShippedDate { get; private set; }

public OrderShippedEvent(int orderId, DateTime shippedDate)
{
OrderId = orderId;
ShippedDate = shippedDate;
}
}

--

--