Giter VIP home page Giter VIP logo

ddd's Introduction

@node-ts/ddd

Domain driven design (DDD) is an approach to software design that values simplicity and modeling code as closely to the business domain as possible. This results in code that can be easily understood by the business and evolved as the needs of the business domain change.

By isolating domain code away from all other concerns of the system like infrastructure, security, transportation, serialization etc; the complexity of the system grows only as large as the complexity of the business or problem domain itself.

If you are new to the concepts of DDD, it's highly recommended to do some background reading to grasp the motivations for this approach and to decide if it suits your application.

If you have already decided to use DDD for your application, this package is part of suite of purpose-built libraries that help you and your teams write large distributed node applications that are maintainable and easy to change.

Installation

Install the @node-ts/ddd package and its dependencies:

npm i @node-ts/ddd @node-ts/logger-core @node-ts/bus-core @node-ts/bus-messages inversify --save

Layers

This library encourages layering code using the onion architecture approach. The main two layers that this libray provides helpers for are the domain and application layers outlined below.

Domain layer

The domain layer sits at the centre of the system. It contains domain objects and domain services. This layer has no technical concerns like infrastructure, authentication, data access etc. The goal of the domain layer is to have a place in your application where code can be writen that models your business domains and rules in a way where those business complexities are kept separate from the rest of your application.

As a result, much of the code that gets written in this layer can be read by non-technical staff meaning that greater collaboration with the domain experts and validation of expected behaviours can be performed. Code here is easily unit testable and isolated from the rest of the application.

The domain layer is composed of one or more domains. Domains are logical boundaries around broad groups of related logic. Each domain is comprised of multiple aggregates, which are clusters of closely related data that model a single entity in the real world. Each aggregate has a root, that represents the single point of access into an aggregate and hosts all the actions that can be performed.

A simple example is a user of a website. In this example an "account" domain is established to encapsulate all aspects of user accounts, billing, profiles, contact details, etc. Users can perform the following actions:

  • register() an account
  • changePassword()
  • disable() their account

We can model that these actions have occured using bus Events (see Bus Messages for more details). Here are the events for those actions:

// user-registered.ts
import { Event } from '@node-ts/bus-messages'
import { Uuid } from '@node-ts/ddd'

export class UserRegistered extends Event {
  static readonly NAME = 'org/account/user-registered'
  $name = UserRegistered.NAME
  $version = 0

  /**
   * A user has registered with the website
   * @param userId Identifies the user who registered
   * @param email used to register the user
   * @param isEnabled if the user can log in with this account
   */
  constructor (
    readonly userId: Uuid,
    readonly email: string
    readonly isEnabled: boolean
  ) {
  }
}
// user-password-changed.ts
import { Event } from '@node-ts/bus-messages'
import { Uuid } from '@node-ts/ddd'

export class UserPasswordChanged extends Event {
  static readonly NAME = 'org/account/user-password-changed'
  $name = UserPasswordChanged.NAME
  $version = 0

  /**
   * A user has changed their password
   * @param userId Identifies the user who changed their password
   * @param passwordChangedAt when the password was changed
   */
  constructor (
    readonly userId: Uuid,
    readonly passwordChangedAt: Date
  ) {
  }
}
// user-disabled.ts
import { Event } from '@node-ts/bus-messages'
import { Uuid } from '@node-ts/ddd'

export class UserDisabled extends Event {
  static readonly NAME = 'org/account/user-disabled'
  $name = UserDisabled.NAME
  $version = 0

  /**
   * A user has disabled their account
   * @param userId Identifies the user who changed their password
   * @param isEnabled if the user can log in to their account
   */
  constructor (
    readonly userId: Uuid,
    readonly isEnabled: boolean
  ) {
  }
}

These events above are broadcasted to the rest of your system, normally with a message bus, each time one of the actions are performed on the aggregate root.

The following is an example implementation of the User domain object:

// user.ts
import { AggregateRootProperties, AggregateRoot, Uuid } from '@node-ts/ddd'
import { UserRegistered, UserPasswordChanged, UserDisabled } from './events'
import { OAuthService } from './services'

export interface UserProperties extends AggregateRootProperties {
  email: string
  isEnabled: boolean
  passwordChangedAt: Date | undefined
}

export class User extends AggregateRoot implements UserProperties {
  email: string
  isEnabled: boolean
  passwordChangedAt: Date | undefined

  // Creation static method. Aggregates are never "newed" up by consumers.
  static register (id: Uuid, email: string): User {
    const userRegistered = new UserRegistered(
      id,
      email,
      true
    )

    const user = new User(id)
    // event is applied to the user object
    user.when(userRegistered)
    return user
  }

  /**
   * Changes the user's password that's used to log in to the site
   * @param oauthService the oauth service that hosts the user account
   * @param newPassword password the user wants to use
   */
  async changePassword (oauthService: OAuthService, newPassword: string): Promise<void> {
    // A domain service is used to perform the actual change of password
    await oauthService.changePassword(this.id, newPassword)

    const userPasswordChanged = new UserPasswordChanged(
      this.id,
      new Date()
    )
    super.when(userPasswordChanged)
  }

  /**
   * Disable the user account so they can no longer log in
   */
  disable (): void {
    const userDisabled = new UserDisabled(this.id, false)
    super.when(userDisabled)
  }
  
  protected whenUserRegistered (event: UserRegistered): void {
    this.email = event.email
    this.isEnabled = event.isEnabled
  }

  protected whenPasswordChanged (event: UserPasswordChanged): void {
    this.passwordChangedAt = event.passwordChangedAt
  }

  protected whenUserDisabled (event: UserDisabled): void {
    this.isEnabled = event.isEnabled
  }
}

This approach to modeling the business domain is well documented. It's clear what actions a user can perform, what the business rules are those actions, and what data updates as a result.

Each time an action method is called on a domain objecft, an event is prepared and applied to the when() protected method. This method does a number of things:

  • It adds the event into the list of new changes made to the aggregate
  • It increments the verison of the aggregate as data has now changed
  • It invokes the method named when<EventName> on the aggregate (eg: whenUserRegistered)

At this point it's important to note that the aggregate has not been persisted nor the event published to the bus. This will be the responsibility of the application server that initially invoked the domain action.

Application layer

The application layer sits around the domain layer. It provides services that act as a gateway to performing actions against the domain. Broadly speaking, these services typically offer one method per command, and can retrieve domain objects from persistence, query other necessary data inputs, gather dependencies to inject and persist data back to the database.

The following UserService provides operations that modify the User domain object. Note that once an operation has been performed on the domain object, it is persisted via its write repository. This operation will store the updated object in the database as well as publishing any events to the bus.

// user-service.ts
import { injectable, inject } from 'inversify'
import { OAuthService } from './services'
import { RegisterUser, ChangePasswordForUser, DisableUser } from './commands'

@injectable()
export class UserService {
  constructor (
    @inject(ACCOUNT_SYMBOLS.UserWriteRepository)
      private readonly userWriteRepository: UserWriteRepository,
    @inject(ACCOUNT_SYMBOLS.OAuthService)
      private readonly oauthService: OAuthService
  ) {
  }

  async register ({ id, email }: RegisterUser): Promise<void> {
    const user = User.register(id, email)
    await this.userWriteRepository.save(user)
  }

  async changePassword ({ id, newPassword }: ChangePasswordForUser): Promise<void> {
    const user = await this.userWriteRepository.getById(id)
    await user.changePassword(this.oauthService, newPassword)
    await this.userWriteRepository.save(user)
  }

  async disable ({ id }: DisableUser): Promise<void> {
    const user = await this.userWriteRepository.getById(id)
    await user.disable()
    await this.userWriteRepository.save(user)
  }
}

The UserWriteRepository injected into the above service comes from the following class definition. The in-built WriteRepository<> is a wrapper around TypeORM that provides the ability to write to different types of databases as well as sending domain object change events to the bus.

// user-write-repository.ts
import { injectable, inject } from 'inversify'
import { Connection } from 'typeorm'
import { LOGGER_SYMBOLS, Logger } from '@node-ts/logger-core'

@injectable()
export class UserWriteRepository extends WriteRepository<User, UserWriteModel> {
  constructor (
    @inject(SHARED_SYMBOLS.DatabaseConnection) databaseConnection: Connection,
    @inject(LOGGER_SYMBOLS.Logger) logger: Logger
  ) {
    super(User, UserWriteModel, databaseConnection, logger)
  }
}

ddd's People

Contributors

adenhertog avatar greenkeeper[bot] avatar snyk-bot avatar yochum avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

ddd's Issues

Introduce value object interface / abstract

Create a new base class (ie; ValueObject<PropertyType>) that can be inherited to create a value object concrete definition.

Value objects are a core concept of DDD and represent things that are identified by their value rather than their hash/id/reference etc. See: https://martinfowler.com/bliki/ValueObject.html

There're already some examples on the web on this practice in typescript (eg https://khalilstemmler.com/articles/typescript-value-object/) that can be used. This task can be used to create a similar base and exposed in ddd.

Side note: operator overloading provides a cleaner way to do this in other languages, but that doesn't appear to be coming to java/typescript: microsoft/TypeScript#6936

Complete a basic example in DDD

Examples are a great way to help people learn a framework. A start of a DDD based example has been created in packages/ddd-example that models a security system. This was selected because it uses real-world concepts that are easy to grasp, and easy to communicate the code layout aspects of DDD.

This is a rather big "epic", so rather than doing everything in a single PR, this task would just be to implement the AlarmSystem agg root and some of its interactions.

Supported node.js version is not specified as prerequisite in README.

I have 2 versions of node.js in my local.

❯ nvm ls
       v10.21.0
->     v14.15.0
         system

But in both versions, yarn test failed in ddd/packages/ddd with same error below:

yarn run v1.22.4
$ jest "(src\/.+\.|/)spec\.ts$"
 FAIL  src/infrastructure/read-repository.spec.ts
  ● Test suite failed to run

    SyntaxError: /Users/poqw/github/ddd/packages/ddd/src/infrastructure/read-repository.spec.ts: Unexpected token, expected "{" (12:47)

      10 | }
      11 |
    > 12 | class UserReadRepository extends ReadRepository<UserModel> {
         |                                                ^
      13 |   constructor (connection: Connection) {
      14 |     super(
      15 |       connection,

      at Parser._raise (../../node_modules/jest/node_modules/@babel/parser/src/parser/error.js:60:45)

 FAIL  src/infrastructure/write-repository.spec.ts
  ● Test suite failed to run

    SyntaxError: /Users/poqw/github/ddd/packages/ddd/src/infrastructure/write-repository.spec.ts: Unexpected reserved word 'interface' (13:0)

      11 | import { Bus } from '@node-ts/bus-core'
      12 |
    > 13 | interface UserProperties extends AggregateRootProperties {
         | ^
      14 |   name: string
      15 |   email: string
      16 | }

      at Parser._raise (../../node_modules/jest/node_modules/@babel/parser/src/parser/error.js:60:45)

 FAIL  src/domain/aggregate-root.spec.ts
  ● Test suite failed to run

    SyntaxError: /Users/poqw/github/ddd/packages/ddd/src/domain/aggregate-root.spec.ts: Support for the experimental syntax 'classProperties' isn't currently enabled (7:9):

       5 |
       6 | class SomethingHappens extends Event {
    >  7 |   $name = 'node-ts/ddd/something-happens'
         |         ^
       8 |   $version = 0
       9 |
      10 |   example = 2

    Add @babel/plugin-proposal-class-properties (https://git.io/vb4SL) to the 'plugins' section of your Babel config to enable transformation.
    If you want to leave it as-is, add @babel/plugin-syntax-class-properties (https://git.io/vb4yQ) to the 'plugins' section to enable parsing.

      at Parser._raise (../../node_modules/jest/node_modules/@babel/parser/src/parser/error.js:60:45)

Test Suites: 3 failed, 3 total
Tests:       0 total
Snapshots:   0 total
Time:        6.884s
Ran all test suites matching /(src\/.+\.|\/)spec\.ts$/i.
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

I think it is related to the version of node(Or not, let me know it first).
What version should I use? and Is there any plan to support node v14 LTE?

Problem installing ddd

I have created a new project, and I tried to install ddd with their dependencies, but I have the following error:

npm i @node-ts/ddd @node-ts/logger-core @node-ts/bus-core @node-ts/bus-messages inversify --save
npm ERR! code ERESOLVE
npm ERR! ERESOLVE unable to resolve dependency tree
npm ERR!
npm ERR! While resolving: [email protected]
npm ERR! Found: @node-ts/[email protected]
npm ERR! node_modules/@node-ts/logger-core
npm ERR! @node-ts/logger-core@"" from the root project
npm ERR! peer @node-ts/logger-core@"^0.0.17" from @node-ts/[email protected]
npm ERR! node_modules/@node-ts/bus-core
npm ERR! @node-ts/bus-core@"
" from the root project
npm ERR! peer @node-ts/bus-core@"^0.6.3" from @node-ts/[email protected]
npm ERR! node_modules/@node-ts/ddd
npm ERR! @node-ts/ddd@"" from the root project
npm ERR!
npm ERR! Could not resolve dependency:
npm ERR! peer @node-ts/logger-core@"^0.1.0" from @node-ts/[email protected]
npm ERR! node_modules/@node-ts/ddd
npm ERR! @node-ts/ddd@"
" from the root project
npm ERR!
npm ERR! Fix the upstream dependency conflict, or retry
npm ERR! this command with --force, or --legacy-peer-deps
npm ERR! to accept an incorrect (and potentially broken) dependency resolution.
npm ERR!
npm ERR! See /.npm/eresolve-report.txt for a full report.
npm ERR! A complete log of this run can be found in:
npm ERR! /.npm/_logs/2021-06-30T09_06_28_182Z-debug.log

Question: Use of `When`

Great package! I especially like the way how events are handled. However, one thing caught my eye.
Within an aggregate events are published. This makes perfect sense as per DDD your not allowed to update different aggregates in the same transaction. To achieve eventual consistencies, events published in one aggregate can be used by services to update other aggregates. However, in this library the aggregate which publishes events also handles its own events (through when() on the abstract aggregate root). I was wondering why this is? Why not simple call a method on the aggregate?

@note-ts/ddd/tsconfig.json file error

the file @node-ts/ddd/tsconfig.json is looking for a tsconfig file in the base node_modules folder, which doesn't exist. This 'file not found' error shows up in my VS Code IDE.

{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "baseUrl": "."
  },
  "exclude": ["dist"]
}

Action required: Greenkeeper could not be activated 🚨

🚨 You need to enable Continuous Integration on Greenkeeper branches of this repository. 🚨

To enable Greenkeeper, you need to make sure that a commit status is reported on all branches. This is required by Greenkeeper because it uses your CI build statuses to figure out when to notify you about breaking changes.

Since we didn’t receive a CI status on the greenkeeper/initial branch, it’s possible that you don’t have CI set up yet. We recommend using Travis CI, but Greenkeeper will work with every other CI service as well.

If you have already set up a CI for this repository, you might need to check how it’s configured. Make sure it is set to run on all new branches. If you don’t want it to run on absolutely every branch, you can whitelist branches starting with greenkeeper/.

Once you have installed and configured CI on this repository correctly, you’ll need to re-trigger Greenkeeper’s initial pull request. To do this, please click the 'fix repo' button on account.greenkeeper.io.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.