Leveraging TypeScript for domain-driven design

mangelosanto

Matt Angelosanto

Posted on January 11, 2024

Leveraging TypeScript for domain-driven design

Domain-driven design (DDD) is a software development approach that aims to simplify the creation of applications that involve complex business logic. In this article, we’ll explore how to leverage TypeScript for DDD. TypeScript’s sophisticated type system enables fine-grained domain modeling and is highly adaptable, lending itself to complex app development.

We’ll dive into the main principles and guidelines of domain-driven design, discuss how TypeScript can assist with DDD, and investigate if DDD can benefit frontend programming. We’ll also take a look at a domain-driven design example written in TypeScript.

What is domain-driven design?

In software engineering, a domain is the specific area of knowledge used by the computer program. In other words, the domain of software is the subject area where the software applies.

For example, if we develop an infotainment application for a car model, the domain will be automotive. Different domains often bring different definitions for the same concepts. For instance, the word “engine” in the automotive domain might convey a different meaning in another domain.

When we model our software based on the specific domain we’re in, we talk about domain-driven design. With this approach, the terms used in our source code, such as class names, method names, and so forth, should match concepts in the business domain. Going back to the automotive infotainment system, we might have classes called Radio, Engine, Wheel, MusicTrack, and so on.

Generally speaking, to structure code like this, programmers must interact with domain experts, to understand the context their software will be working within.

Principles of domain-driven design

The main aim of domain-driven design is to simplify the creation of complex applications by combining several, smaller, pieces of software into a business model.

Here are the main principles of DDD:

  • The primary focus is on the core domain and domain logic. This means we have to identify what the core domain or main area of knowledge, is, both in terms of information and behavior
  • The software design is based on a model of the domain. Once we identify the core domain and model it in our software, the rest of the design should follow
  • Technical and domain experts must collaborate to iteratively refine a conceptual model of the domain. The input of the domain experts helps the programmers understand how to model the core domain

The main building block for business models are entities, or objects that have an important meaning in the domain of interest. Several other object types work on entities. For instance, factories are components that are meant to create a single entity or a group of them, hiding the initialization details from the users.

Similarly, repositories deal with persisting entities somewhere, for example on a database. Lastly, services model specific operations in a business logic.

In the automotive domain, for instance, we may have a MusicTrack entity modeling a music track stored somewhere in our car infotainment system. A MusicTrackRepository class could be responsible for storing new music tracks, retrieving information on existing tracks, and deleting them. Similarly, we might have a MusicService to play selected tracks.

Pros and cons of domain-driven design

The main benefit of domain-driven design is that the main concepts, or entities, are defined at the very beginning of the project. This leads to easy communication because everything has a fixed meaning and there are no ambiguities.

Furthermore, modeling entities, services, factories, and repositories with objects, while following the principles of object-oriented design, results in a codebase that’s easier to maintain in the long run. Typically, each component has a well-defined scope and responsibility, enhancing the encapsulation and modularity of the software.

However, DDD requires very strong domain-related knowledge. It lends itself to projects with very complex business logic, like our infotainment system.

Projects with very complex technical requirements (e.g., performance) or with a relatively simple business logic (e.g., an embedded system for processing only a few signals) are generally less suitable for DDD.

How can domain-driven design benefit from TypeScript?

Domain-driven design is suitable for problems with complex business logic. TypeScript’s powerful type system enables very fine-grained domain modeling. Several TypeScript features are useful in this regard, like record types, union and intersection types, tuples, literal types, and generics.

By leveraging these features when needed, we can write type-safe, readable, and maintainable codebases. A clean and well-defined domain model also offers the benefit of being a useful documentation tool.

One of TypeScript’s primary strengths is that its sophisticated type system lets us pick features as needed, employing and adapting the language to several software design frameworks.

Domain-driven design and frontend

Domain-driven design offers a nice way to design our business model. With DDD, the complexity of the backend grows along with the business requirements, keeping the model and the requirements aligned.

Whether or not this is also true for frontend codebases depends on several factors. Many frontend codebases deal more with technical complexity, like choosing the right technology stack, rather than with the complexity of the business domain.

It’s often preferable to centralize the domain model in the backend, possibly creating subdomains (i.e., projections of the main domain model) dedicated to the frontend. This way, the frontend won’t contain any reference to the business logic.

Some frontend projects may not have a backend, in which case, domain-driven design can be a good choice. Furthermore, it may be beneficial to also leverage micro-frontends, which are quite similar to microservices and enable us to break down a complex frontend into smaller, simpler parts.

When a micro-frontend does not have a backend behind it, leveraging DDD can help further simplify the domain complexity of the application.

An example of domain-driven design

To better understand how TypeScript can be used for data-driven design, let’s take a look at a simple application that is used to manage sports competition records.

The entity

Entity objects are the main components of the domain model. Here’s an example of an entity from our TypeScript application, recording the best times of the competitors:

class Competition {
    readonly id: string
    readonly maleRecordInSeconds: number
    readonly femaleRecordInSeconds: number

    constructor(mr: number, fr: number) {
                this.id = ""
                this.maleRecordInSeconds = mr
                this.femaleRecordInSeconds = fr
    }
}
Enter fullscreen mode Exit fullscreen mode

The repository

We can use repository components to retrieve entities, or aggregates of entities, from a means of storage, such as a database. The goal of this type of component is to hide the complexity of the underlying storage layer. Ideally, they should be declared as interfaces, so that we can easily swap one concrete implementation for another:

interface CompetitionRepository {
    getAll(): Competition[]
    addOne(c: Competition): boolean
    // more CRUD-based functions here
}

class MySQLCompetitionRepository implements CompetitionRepository {
    getAll(): Competition[] {
        // ...
    }

    addOne(c: Competition): boolean {
        // ...
    }
}
Enter fullscreen mode Exit fullscreen mode

The service

We can use service classes to model operations that do not belong to any other objects. For example, they can be the entry point to model and implement the use cases of our application. In this case, their implementations make use of different repositories to fetch the entities and possibly modify them:

class CompetitionService {
    function setNewFemaleRecordForCompetition(compId: string, fr: number): Competition {
                // 1\. get competition from DB using MySQLCompetitionRepository
                // 2\. set new record
                // 3\. store new object in the DB
                // 4\. return new object
    }
}
Enter fullscreen mode Exit fullscreen mode

Layers

When organizing the TypeScript code, we can divide it into different layers. Here are the most widely used layers:

  1. Domain: Contains our business model, as well as the definitions of the repositories (not the current implementations!)
  2. Infrastructure: Contains the implementation of the repository classes, as well as any other component that depends on the actual application context (e.g., message brokers)
  3. Application: Defines the services and models the use cases of the application
  4. Presentation: User interface-related logic, if any

Conclusion

In this article, we explored the main principles, terminology, and concepts associated with modeling software according to domain-driven design. We discussed cases where DDD is most beneficial, and when it’s more advantageous to look for another modeling pattern.

We also mentioned some lifesaving TypeScript features that turn out to be very useful in modeling complex business domains and explored the idea of applying domain-driven design to frontend and micro-frontend projects.

Even though DDD is more backend-oriented, we can apply its principles and ideas to the frontend world as well. Lastly, we reviewed a simple, yet practical, example of DDD in TypeScript.

Domain-driven design is just a set of guidelines to model our business domain and regulate the way we develop an application. Of course, no design pattern will ever apply to all domains, scenarios, and application types. In fact, each pattern is just another tool on our belt. Modern software design is an iterative process and we, as engineers and developers, can select one pattern over the other.


LogRocket: Full visibility into your web and mobile apps

LogRocket Signup

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.

Try it for free.

💖 💪 🙅 🚩
mangelosanto
Matt Angelosanto

Posted on January 11, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

What was your win this week?
weeklyretro What was your win this week?

November 29, 2024

Where GitOps Meets ClickOps
devops Where GitOps Meets ClickOps

November 29, 2024

How to Use KitOps with MLflow
beginners How to Use KitOps with MLflow

November 29, 2024