Design Principles

Approach to building Bounded Contexts

Background

The design of all Bounded Contexts will follow a Domain Driven Design (DDD) pattern. This will follow a hexagonal type architecture following a similar pattern as in this tutorial.

The rest of this document details the principles we have agreed to follow, in order to have a common approach when in comes to coding.

Layering

Traditionally there are the following layers:

  • UI Layer - this is handled by the Reviewer Client

  • Application Layer - Any Bounded Context (for example Submission) will expose a number of resolvers that will provide the Port/Adapter necessary for Reviewer Client to connect. This layer provides the functionality to perform the Business operations in terms of using the Domain Services within the Domain Layer.

  • Domain Layer - Provides functionality in domain-parlance, this aids development in terms of the ubiquitous language.

  • Infrastructure Layer - Provides the raw CRUD operations for persisting the state of the Domain.

Taking the 3 layers above that are implemented in terms of the bounded context on the server, these will be expressed in-terms of the following classifications.

Software Concepts

Resolvers

Responsible for exposing the functionality over GraphQL

Principles

  • Network Security

  • Delgation to Service(s)

Example

const resolvers = (submissionService: SubmissionService): IResolvers => ({
    Query: {
        async getSubmissions(): Promise<DtoViewSubmission[] | null> {
            return await submissionService.findAll();
        },
    ...

Application Services

Responsible for providing the application's functionality.

The Application Services should contain functions to provide a one-to-one mapping of the functionality exposed by the Resolvers.

  • Services - These

Principles

  • Application Security / Permissions

  • Needs to be stateless - Transactional Management?

  • Services are hierarchical, i.e. services may contain / use other services.

Example

class SubmissionService {
    ...
    submitManuscript(userId, submissionId) {
        if (!userService.hasPermission(userId, SUBMIT) ||
            !userService.isAuthor(userId, submissionId)) {
            throw 
        }
        return exportService.export(userId, submissionId)
    }
    ...    
}

Domain Model / Services

Currently we are not discriminating between "Application Services" and "Domain Services"

Repositories

Responsible for storing the state of the Domain

Principles

  • Data Mappers are internal to the repository.

  • Any internal peculiarities of storing data are encasulated here (don't leak)

Example

export class KnexSubmissionRepository implements SubmissionRepository {
    private readonly TABLE_NAME = 'manuscript';

    public constructor(private readonly knex: Knex<{}, unknown[]>) {}
    ...
    public async update(sub: Submission): Promise<Submission | null> {
        const submission = await this.findById(sub.id);
        if (submission === null) {
            return null;
        }
        const dtoSubmission: DtoSubmission = SubmissionMapper.toDto(sub);
        const dtoToSave = { ...SubmissionMapper.toDto(submission), ...dtoSubmission, updated: new Date() };
        await this.knex
            .withSchema('public')
            .insert(dtoToSave)
            .into(this.TABLE_NAME);

        return SubmissionMapper.fromDto(dtoToSave);
    }
...

Last updated