DEV Community

paulmojicatech
paulmojicatech

Posted on

Domain Driven Design with NGRX

What is Domain Driven Design?

Domain Driven Design is a method of implementing software around a construct called domain models. These models define the business rules and processes around a specific domain. This method differs from other methods in that it puts the development team in direct exposure to the business and the stakeholders.

What is NgRx?

NgRx is is Angular's implementation of the redux pattern to manage state within an application. There are 3 main pieces to NgRx:

  • Actions:
    • Actions are events that are dispatched to update the state of the application.
  • Reducers:
    • Reducers are pointers to the current state of the application. We should be able to stitch our reducers (or state stores) to get an accurate representation of the current state of the application.
  • Effects:
    • These are listeners to actions (or events) that execute code to mutate (or cause side effects) the state of the application. A canonical example is when an action is dispatched to load data, an effect listens for that action and makes an HTTP call to fetch the data, and then dispatches another action stating that the HTTP call was completed successfully or failed, thus updating the state of the application.
    • An effect takes an observable (the action dispatched it was listening for) and returns another observable (the action with the payload data that will update the state).

Event Storming

Domain driven design has a concept of event storming. The idea around event storming is bringing the business and the dev team together to create an artifact the describes the business rules in the terms of domain events happening in the system. The events are put on a board (either physical or digital) in a linear, time based sequence. This will be the artifact that is delivered at the end of the event storming meeting/s.

How Does NgRx Fit In?

NgRx relies heavily (even in the name) on RxJs. RxJs is the javascript implementation of the Reactive programming pattern. This pattern provides a declarative approach to coding software where event streams flow through the system and based on these events code is executed. One can, hopefully see, how domain events and the Reactive programming pattern can complement each other. Consider the requirements below:

The Requirements

We are building a native application using Ionic so we can leverage our team's web skills. Our application will allow a user to both create a grocery list of items to get and also keep track of grocery list items that have been bought and are in the house.

We want the lists to be stored in device storage so no internet is needed to use the app.

Below is a part of the artifact created during event storming that describes the situation when a user wants to move an item from the items to get list to the current items in the house list.

Image description

From event storming to implementation

Domain driven design use a programming paradigm called CQRS, which stands for Command Query Responsibility Separation. This is the pattern of separating the responsibilty of updating (or adding or deleting) in the system from the reading (or querying) what is already in the system.

To me, this has a pretty clear mapping to NgRx where our effects will be our update models and our reducers / selectors will be our read models. The actions that are dispatched are our domain events occurring in any given time and are dispatched through user interaction.

Dispatch the action:

<pmt-mobile-toolbar title="Things to Get" [actionItems]="['create-outline']"
    (actionItemEvent)="itemsToGetStateSvc.setIsModalOpen(true)">
</pmt-mobile-toolbar>

<ion-content *ngIf="viewModel$ | async as viewModel">
    <pmt-input placeholder="Search" [isMobile]="true" (changeEvent)="itemsToGetStateSvc.handleSearchValueUpdated($event)"></pmt-input>
    <ion-list *ngIf="viewModel.itemsNeeded?.length; else noItemText">
        <ion-item *ngFor="let item of viewModel.itemsNeeded | pmtSearchValue : 'name' : viewModel.searchValue!" lines="full">
            <div class="grocery-item-container">
                <ion-checkbox (ionChange)="itemsToGetStateSvc.removeItemFromItemsToGet(item)" class="checkbox"></ion-checkbox>
                <span class="item-name">{{item.name}}</span>
                <span class="qty">Qty: {{item.qty}}</span>
            </div>
        </ion-item>
    </ion-list>
    <ion-modal #ionModal [isOpen]="viewModel.isModalOpen">
        <ng-template>
            <pmt-mobile-toolbar title="Add item" [actionItems]="['close-outline']" (actionItemEvent)="itemsToGetStateSvc.setIsModalOpen(false)"></pmt-mobile-toolbar>
            <div class="form-container">
                <form novalidate [formGroup]="itemsToGetForm">
                    <div class="autocomplete-container">
                        <pmt-autocomplete (valueChangedEv)="handleAutocompleteChangeEv($event)" [allItems]="viewModel.allAvailableItems" label="Enter an item"></pmt-autocomplete>
                    </div>
                    <pmt-input formControlName="qty" label="Qty"></pmt-input>
                    <div class="action-container">
                        <button [disabled]="itemsToGetForm.invalid" mat-raised-button color="primary" (click)="addItem()">Add Item</button>
                    </div>
                </form>
            </div>
        </ng-template>
    </ion-modal>
    <ng-template #noItemText>
        <main class="no-item-section">
            <div>
                {{viewModel.noItemsText}}
            </div>
        </main>
    </ng-template>

</ion-content>

Enter fullscreen mode Exit fullscreen mode
import { Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
import {
  CurrentGroceryItem,
  GroceryItem,
} from '@pmt/grocery-list-organizer-shared-business-logic';
import {
  BehaviorSubject,
  ignoreElements,
  map,
  merge,
  Observable,
  tap,
} from 'rxjs';
import {
  addItemToGet,
  loadItemsToGet,
  removeItemToGet,
  setIsItemsToGetModalOpen,
} from './actions/items-to-get.actions';
import {
  getAllAvailableItems,
  getIsAddItemsToGetModelOpen,
  getItemsToGet,
} from './index';
import { ItemsToGetState } from './models/items-to-get-state.interface';
import { ItemsToGetViewModel } from './models/items-to-get.interface';

@Injectable()
export class ItemsToGetStateService {
  readonly INITIAL_STATE: ItemsToGetViewModel = {
    itemsNeeded: [],
    noItemsText: 'You currently do not have any items on your grocery list.',
    isModalOpen: false,
    allAvailableItems: [],
    searchValue: undefined,
  };

  private _viewModelSub$ = new BehaviorSubject<ItemsToGetViewModel>(
    this.INITIAL_STATE
  );
  viewModel$ = this._viewModelSub$.asObservable();

  constructor(private _store: Store<ItemsToGetState>) {}

  getViewModel(): Observable<ItemsToGetViewModel> {
    this._store.dispatch(loadItemsToGet());
    const items$ = this._store.select(getItemsToGet).pipe(
      tap((items) => {
        this._viewModelSub$.next({
          ...this._viewModelSub$.getValue(),
          itemsNeeded: items,
        });
      }),
      ignoreElements()
    );
    const isModalOpen$ = this._store.select(getIsAddItemsToGetModelOpen).pipe(
      tap((isOpen) => {
        this._viewModelSub$.next({
          ...this._viewModelSub$.getValue(),
          isModalOpen: isOpen,
        });
      }),
      ignoreElements()
    );
    const allAvailableItems$ = this._store.select(getAllAvailableItems).pipe(
      map((allAvailableItems) => {
        return allAvailableItems.map((item) => item.name);
      }),
      tap((allAvailableItems) => {
        this._viewModelSub$.next({
          ...this._viewModelSub$.getValue(),
          allAvailableItems,
        });
      }),
      ignoreElements()
    );

    return merge(this.viewModel$, items$, isModalOpen$, allAvailableItems$);
  }

  setIsModalOpen(isOpen: boolean): void {
    this._store.dispatch(setIsItemsToGetModalOpen({ isOpen }));
  }

  addItem(itemToAdd: GroceryItem): void {
    this._store.dispatch(addItemToGet({ item: itemToAdd }));
    this._store.dispatch(setIsItemsToGetModalOpen({ isOpen: false }));
  }

  removeItemFromItemsToGet(itemToRemove: CurrentGroceryItem): void {
    this._store.dispatch(removeItemToGet({ itemToRemove }));
  }

  handleSearchValueUpdated(searchValue: string): void {
    this._viewModelSub$.next({
      ...this._viewModelSub$.getValue(),
      searchValue,
    });
  }
}

Enter fullscreen mode Exit fullscreen mode

Things to Note:

  • This shows the HTML template for the items to get screen component. The component has a localized service (ItemsToGetStateService) that handles the business logic for the component. The template calls the service to removeItemFromItemsToGet when the checkbox (ion-checkbox) is checked. The implementation for that method is to simply dispatch the removeItemToGet action.

Actions:


export enum CurrentItemActionType {
  ADD_ITEM_TO_CURRENT_LIST = '[Current] Add Item to Current List'
}

export const addItemToCurrentList = createAction(
  CurrentItemActionType.ADD_ITEM_TO_CURRENT_LIST,
  props<{ itemToAdd: CurrentGroceryItem }>()
);

export enum ItemsToGetActionType {
  REMOVE_ITEM_TO_GET = '[Items to Get] Remove Item to Get',
}

export const removeItemToGet = createAction(
  ItemsToGetActionType.REMOVE_ITEM_TO_GET,
  props<{ itemToRemove: GroceryItem }>()
);


Enter fullscreen mode Exit fullscreen mode

Things to Note:

  • We created two state stores (one for current list and one for items to get). While this keeps the actions, effects, and reducers separate, we can still listen for events (or actions) from either store as long as the EffectsModule is already registered.
  • We have one action in each store, one to add an item to the current items list, and one to remove an item from the items to get list.

Current Item Effects:

@Injectable()
export class CurrentGroceryItemsEffects {
  constructor(
    private _actions$: Actions,
    private _currentItemsUtilSvc: CurrentGroceryItemsUtilService
  ) {}


  addItemToCurrentListUpdateStorage$ = createEffect(
    () =>
      this._actions$.pipe(
        ofType(addItemToCurrentList),
        tap((action) => {
          this._currentItemsUtilSvc.addItemToCurrentListOnStorage(
            action.itemToAdd
          );
        })
      ),
    { dispatch: false }
  );

}
Enter fullscreen mode Exit fullscreen mode

Current Item Util Service


import { Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
import {
  CurrentGroceryItem,
  IonicStorageService,
  IonicStorageType,
} from '@pmt/grocery-list-organizer-shared-business-logic';
import { filter, forkJoin, from, map, take } from 'rxjs';
import { loadCurrentItemsSuccess } from '../actions/current-grocery-items.actions';
import { CurrentListState } from '../models/current-list.interface';

@Injectable({
  providedIn: 'root',
})
export class CurrentGroceryItemsUtilService {
  constructor(
    private _store: Store<CurrentListState>,
    private _storageSvc: IonicStorageService
  ) {}


  addItemToCurrentListOnStorage(itemToAdd: CurrentGroceryItem): void {
    this._storageSvc
      .getItem(IonicStorageType.CURRENT_ITEMS)
      .pipe(take(1))
      .subscribe((itemsStr) => {
        const currentItems = itemsStr
          ? [...JSON.parse(itemsStr), itemToAdd]
          : [itemToAdd];
        this._storageSvc.setItem(
          IonicStorageType.CURRENT_ITEMS,
          JSON.stringify(currentItems)
        );
      });
  }

}

Enter fullscreen mode Exit fullscreen mode

Things to Note:

  • We inject the util service into the effects. In the util service, we inject both the store and the storage service, where the store allows us to query the store for the current state of the application and the storage stores the items to device storage.
  • The effects listen for the addItemToCurrentList action to be dispatched, then calls the util service to execute code. We also specify that the effect {dispatch: false}. Since an effect takes in an observable and returns an observable, if we did not specify {dispatch: false}, we would find ourselves in an endless loop.

Items To Get Effects


@Injectable()
export class ItemsToGetEffects {
  constructor(
    private _actions$: Actions,
    private _storageSvc: IonicStorageService,
    private _itemsToGetUtilSvc: ItemsToGetUtilService
  ) {}

  removeItemFromItemsToGetUpdateStorage$ = createEffect(
    () =>
      this._actions$.pipe(
        ofType(removeItemToGet),
        switchMap((action) =>
          this._storageSvc.getItem(IonicStorageType.ITEMS_TO_GET).pipe(
            tap((itemsStr) => {
              const itemsToGet = (
                JSON.parse(itemsStr) as CurrentGroceryItem[]
              ).filter((item) => item.name !== action.itemToRemove.name);
              this._storageSvc.setItem(
                IonicStorageType.ITEMS_TO_GET,
                JSON.stringify(itemsToGet)
              );
            })
          )
        )
      ),
    { dispatch: false }
  );

  removeItemFromItemsToGetAddItemToCurrentList$ = createEffect(() =>
    this._actions$.pipe(
      ofType(removeItemToGet),
      map((action) => {
        const itemToAdd: CurrentGroceryItem = {
          ...action.itemToRemove,
          id: `${new Date()}_${action.itemToRemove.name}`,
          datePurchased: new Date().toDateString(),
        };
        return addItemToCurrentList({ itemToAdd });
      })
    )
  );
}

Enter fullscreen mode Exit fullscreen mode

Things to Note:

  • We create 2 effects to listen for one action (removeItemToGet). When this action is dispatched, we have one effect that where we use {dispatch: false} to update the device storage.
  • The other effect dispatches the addItemToCurrentList action, which we listen for in our effect we discussed above.

Reducers:


const initialState: CurrentListState = {
  currentItems: undefined,
};

export const currentGroceryItemsReducer = createReducer(
  initialState,
  on(addItemToCurrentList, (state, { itemToAdd }) => {
    const updatedItems = [...(state.currentItems ?? []), itemToAdd];
    return { ...state, currentItems: updatedItems };
  })
);

const initialState: ItemsToGetState = {
  itemsToGet: [],
  isLoaded: false,
  isAddItemModalVisible: false,
  allAvailableItems: [],
};

export const itemsToGetReducer = createReducer(
  initialState,
  on(removeItemToGet, (state, { itemToRemove }) => {
    const itemsToGet = state.itemsToGet.filter(
      (item) => item.name !== itemToRemove.name
    );
    return { ...state, itemsToGet };
  })
);

Enter fullscreen mode Exit fullscreen mode

Things to Note:
We have 2 reducers that update our 2 stores (or read models for CQRS folks) when the 2 actions are dispatched.

Conclusion

In this article we showed how we can think about how the implementation to NgRx can be similar to the implementation of domain driven design. Both NgRx and Domain Driven Design rely heavily on events occurring in the system to derive the state of the system / application. We can also see how NgRx is similar to the CQRS (Command Query Responsibility Separation) that is a tenant of Domain Driven Design.

Discussion (0)