Mambelli Domain Model

giacomo-avatar nicolo-avatar nicolas-avatar linda-avatar

Introduction

We decided to model the domain of the Mambelli cheese factory:
a small family business that produces and sells cheese.

We chose this domain because:

  • It is quite complex
  • There are domain experts that could help us
  • We ❤️ cheese

Domain Modeling

event storming session

Event Storming

event storming diagram

Core Domain Chart

core domain chart

Context Map

context map

Architecture

The architecture of each bounded context follows the Clean Architecture’s structure

clean architecture

DevOps

DVCS Workflow

We adopted a git-flow-like workflow with a beta branch for pre-releases and a main branch for the stable ones

%%{ init: { 'theme':'base', 'themeVariables': { 'git0': '#ff3c3e', 'git1': '#f9b59c', 'git2': '#8de0d8', 'git3': '#fca55f', 'git4': '#55303e', 'gitBranchLabel0': '#000000', 'gitBranchLabel1': '#000000', 'gitBranchLabel2': '#000000', 'gitBranchLabel3': '#000000', 'gitBranchLabel4': '#ffffff' } } }%% gitGraph commit id: "chore: ..." commit id: "build: ..." commit id: "chore: ..." commit id: "ci: ..." branch beta commit tag: "1.0.0-beta.1" branch feat/feature-1 checkout feat/feature-1 commit id: "feat: feature 1" commit id: "feat: feature 2" checkout beta merge feat/feature-1 tag: "1.0.0-beta.2" branch feat/feature-2 commit id: "feat: feature 3" checkout beta merge feat/feature-2 tag: "1.0.0-beta.3" checkout main merge beta tag: "1.0.0" branch fix/fix-2 commit id: "fix: fix1" commit id: "fix: fix2" checkout main merge fix/fix-2 tag: "1.0.1"

Conventional Commits

To enforce Conventional Commits we developed a gradle plugin and an sbt plugin. The plugin is handy since:

  • It creates a git hook as soon as the project is imported (you don’t forget to set it up!)
  • It can also be configured through plug-in keys

Semantic Release

semantic-release automates the whole package release workflow:

  • Determines automatically the next version number
  • Generates the release notes
  • Publishes the artifacts (Maven Central and Docker Hub in our case)

The use of Conventional Commits combined with Semantic Release helped us to automate the release process

Quality Assurance

Code quality is an important aspect, so the following tools have been used:

Each of these tools is used in CI to prevent the merging of bad code

Continuous Integration and Deployment

The following workflow is the result of a pipeline optimization that seeks to take full advantage of the parallelism between jobs:

%%{init: {'theme':'base', 'themeVariables': { 'fontFamily': 'Inter' }}}%% flowchart LR SFMT(Scalafmt) --> B SFX(Scalafix) --> B WRM(Wartremover) --> B T(Unit test) --> Cov(Coverage Report) Cov --> B A(Documentation site)-->B(Publish) B --> C(Publish site)

To simplify the Publish job, the scala-release action was developed

Project Management

  • We first defined a product backlog and biweekly sprint backlogs
  • All backlog items are tracked by linked GitHub issues
  • Closing PRs and issues automatically advances the project status

Development Choices

Domain Modeling Approach

All core domain concepts are modelled using simple ADTs to keep them as simple and adherent to the expert’s definition as possible:

enum Batch:
  case Aging(id: BatchID, cheeseType: CheeseType, readyFrom: LocalDateTime)
  case ReadyForQualityAssurance(id: BatchID, cheeseType: CheeseType)

instead of the possibly confusing:

final case class Batch(
  id: BatchID,
  cheeseType: CheeseType,
  isAging: Bool,
  readyFrom: Option[LocalDateTime], // only defined if isAging 
)

All primitive types are not only wrapped in appropriate value objects but also enriched with compile-time-checked predicates:

type PositiveNumber = Int Refined Positive
final case class InStockQuantity(n: PositiveNumber)

instead of:

final case class InStockQuantity private(n: Int)
object InStockQuantity:
  def apply(n: Int): Option[InStockQuantity] =
    if n < 0 then None else Some(InStockQuantity(n))

Side-effect Encoding via Monads

All core domain actions use a monadic encoding of side-effects: all side effects are reified (using an mtl-style encoding) and expressed in the type signature of the function

def labelProduct[M[_]: CanRaise[WeightNotInRange]: CanEmit[ProductStocked]: Monad]
  (...): M[LabelledProduct] =
for
  ...
  product <- optionalProduct.ifMissingRaise(WeightNotInRange(...): WeightNotInRange)
  labelledProduct = LabelledProduct(product, AvailableQuantity(1), batch.id)
  _ <- emit(ProductStocked(labelledProduct): ProductStocked)
yield labelledProduct

DTOs

DTOs play a fundamental role in transferring data to and from a bounded context. All the repetitive code needed to generate a DTO instance is automatically generated taking advantage of Scala 3’s metaprogramming and inlining

final case class Client(code: ClientCode, name: String, vatNumber: VATNumber)
final case class ClientDTO(code: String, name: String, vatNumber: String)

// Automatic derivation with compile-time checks!
given DTO[Client, ClientDTO] = productTypeDTO

HTTP API

We used the tapir library to define the endpoints. It proved to be useful in many ways:

  • The endpoints are defined declaratively
  • All the endpoints definitions are type-checked
  • It automatically generates the OpenAPI specification and displays it through Swagger
// GET order/{order-id}/ddt
val getTransportDocumentEndpoint: PublicEndpoint[String, String, TransportDocumentDTO, Any] =
  endpoint.get
    .in("order")
    .in(
      path[String]
        .description("The ID of the order for which the transport document is requested")
        .name("order-id"),
    )
    .in("ddt")
    .out(jsonBody[TransportDocumentDTO].description("The transport document for the given order"))
    .errorOut(stringBody)

Documentation

  • All ubiquitous language concepts are mirrored by a corresponding ADT in the code
  • Keeping the code and the ubiquitous language up-to-date can be a difficult task
  • The code should be the only source of truth (the code is the ubiquitous language)

We needed a way to automatically generate documentation pages containing the ubiquitous language definitions coming from the scaladoc

Ubidoc

A configurable plugin we developed to create markdown tables from ubiquitous language definitions from the scaladoc

/**
 * It defines, for each product, the [[StockedQuantity quantity available in stock]].
 */
final case class Stock(...)
/**
 * A quantity of a stocked product, it may also be zero.
 */
final case class StockedQuantity(...)

is turned into this table:

TermDefinition
StockIt defines, for each product, the quantity available in stock.
Stocked QuantityA quantity of a stocked product, it may also be zero.

License

The open licence that best suited our needs was the Apache Licence 2.0:
our codebase could contain refences to the Mambelli trademark and this licence does not grant permission to use it
"…except as required for reasonable and customary use
in describing the origin of the Work…"