Mambelli Domain Model
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:
The architecture of each bounded context follows the Clean Architecture’s structure
We adopted a git-flow
-like workflow with a beta
branch for pre-releases and a main
branch for the stable ones
To enforce Conventional Commits
we developed
a gradle plugin and an
sbt plugin.
The plugin is handy since:
semantic-release
automates the whole package release workflow:
Maven Central
and Docker Hub
in our case)The use of Conventional Commits combined with Semantic Release helped us to automate the release process
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
The following workflow is the result of a pipeline optimization that seeks to take full advantage of the parallelism between jobs:
To simplify the Publish
job, the scala-release
action was developed
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))
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 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
We used the tapir library to define the endpoints. It proved to be useful in many ways:
// 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)
We needed a way to automatically generate documentation pages containing the ubiquitous language definitions coming from the scaladoc
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:
Term | Definition |
---|---|
Stock | It defines, for each product, the quantity available in stock. |
Stocked Quantity | A quantity of a stocked product, it may also be zero. |
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…"