Project Structure & Build System¶
This project uses Gradle as its build system.
Project Structure¶
Munchies is a monorepo with a multi project structure. These are the project available inside the repository:
architecture-rules(Kotlin): contains Konsist's architectural tests for a clean DDD architecturecommons(Multiplatform): contains common code for all other projectse2e-test(Kotlin): contains end-to-end tests for the services- frontend-service (TypeScript): contains the code for the frontend (INCOMPLETE)
gateway-service(TypeScript): contains the gateway's microservice codegateway-shared(Multiplatform): contains the gateway's API signaturesmicronaut-commons(Kotlin): contains Micronaut beans shared across every JVM microservicenotification-service(TypeScript): contains the notification's microservice codenotification-shared(Multiplatform): contains the notification's API signaturesorder-service(Kotlin): contains the order's microservice codeorder-shared(Multiplatform): contains the order's API signaturespayment-service(TypeScript): contains the payment's microservice codepayment-shared(TypeScript): contains the payment's API signaturesrestaurant-service(Kotlin): contains the restaurant's microservice coderestaurant-shared(Multiplatform): contains the restaurant's API signatures- table-reservation-service (TypeScript): contains the table reservation's microservice code (INCOMPLETE)
- table-reservation-shared (Multiplatform): contains the table reservation's API signatures (INCOMPLETE)
- scheduler-service (Typescript): contains the order scheduler microservice code (INCOMPLETE)
user-service(Kotlin): contains the user's microservice codeuser-shared(Multiplatform): contains the user's API signatures
Meanwhile, these folders:
build-logic: contains the Gradle conventions, tasks and configurations for the projectsconfig: contains the detekt and docker images configurationsdocs: contains the internal and external documentationhelm: contains themunchies-serviceHelm chart, a generic Deployment + Service (+ optional HPA and MongoDB StatefulSet) parameterized per microservice through the values files inhelm/values; it replaces the duplicated raw manifests that used to live underk8s/order-service,k8s/user-serviceandk8s/restaurant-servicek8s: contains the Kubernetes configurationsloadtest: contains a minikube load testing configuration with autoscalingobservability: contains a scoped-down Prometheus + Grafana Helm-based stack (brought up only around a benchmark run, not part ofk8s/or./gradlew deploy), with a Grafana dashboard showing ready replicas, HPA current vs desired replicas and per-pod CPU usage against its requestscripts: contains the script for building documentation, publishing to NPM, DockerHub, Maven and a script to deploy and run our project.
Build System¶
Our project heavely relies on Gradle to test, compile, build artifacts and run.
We've written custom tasks that allow to link and execute commands in different platform such as the JVM and Node;
this was possible by a plugin which allows Gradle to run npm commands.
We linked the ./gradlew build command to also run a TypeScript project's npm run build using a
multiplatform's generated JavaScript code, which is beforehand compressed in a .tgz archive in order to have the
up-to-date code.
We've also created tasks to create the service's dockerfiles', their images and link them via a
./gradlew composeUp to run the whole project with a single command, furthermore showDb tasks were created to
better analyze the mongodb containers running.
These dockerfiles and images, were also used to be able to utilize kubernetes as a deployment method instead of docker
(see Deployment).
We've also created two tasks to help during the deploy-docs workflow that copies the generated docs to be then further translated into a web page.
These are all the tasks we've created:
Munchies tasks
--------------
composeBuild - Builds images for services of docker-compose project
composeDown - Stops and removes containers of docker-compose project (only if stopContainers is set to true)
composeShowDb - Shows MongoDB data for a service. Usage: ./gradlew composeShowDb -Pservice=<name> [-Pcollection=<name>]
composeUp - Builds and starts containers of docker-compose project
deploy - Deploys all services to Minikube. Usage: ./gradlew deploy
deployServices
dockerBuild
dockerCreate
graphDump - Dumps project dependencies to a mermaid file.
graphUpdate - Updates Markdown file with the corresponding dependency graph.
k8sInfo - Prints the current pods and deployments across all namespaces in Minikube (except for kubernetes' pods).
moveJsDeps
pack_commons
pack_gateway-shared
pack_notification-shared
pack_order-shared
pack_payment-shared
pack_restaurant-shared
pack_table-reservation-shared
pack_user-shared
printJsDeps
run
showDb - Shows MongoDB data for a specific service. Usage: ./gradlew showDb -Pservice=<name> [-Pcollection=<name>]
showKf - Tails a Kafka topic from Minikube. Usage: ./gradlew showKf -Ptopic=<name>
test
typeDocs
undeploy - Undeploys all services from Minikube. Usage: ./gradlew undeploy [-PwipeData=true]
undeployServices
vitestCoverageVerify
Lastly, we've created a ./gradlew graphUpdate task which updates a
README.md file for each Gradle
subproject that displays its dependencies to other subprojects.
An example:
---
config:
layout: elk
elk:
nodePlacementStrategy: SIMPLE
---
graph TB
:payment-shared[payment-shared]:::unknown
:payment-service[payment-service]:::unknown
:commons[commons]:::unknown
:payment-service -.->|jsImplementation| :commons
:payment-service -.->|jsImplementation| :payment-shared
:payment-shared -.->|commonMainImplementation| :commons
classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000;
Shared build logic¶
Many subproject share the same build logic configuration, that are defined in the build-logic folder, these
conventions also have a hierarchical structure.
These are the current build conventions available:
dokka-convention: dokka configuration for jvm projectsexpress-server: configuration for TypeScript projectskotlin-jvm: base jvm configurationlinter-convention: linter for both Kotlin and TypeScriptmaven-publish-convention: configuration for maven publishingmicronaut-base: micronaut base pluginsmicronaut-library: minimal Micronaut setup (KSP annotation processing + allopen) for a plain library module that contributes beans to every service, without theapplication/Docker/AOT machinery a full service needsmicronaut-server: Kotlin micronaut service configurationmultiplatform-base: Kotlin Multiplatform configurationmunchies-subproject: subproject with dependency READMEtest-suites: component and integration configuration for Micronaut services
flowchart TB
ms["micronaut-server"]:::pj
dk["dokka-convention"]
kj["kotlin-jvm"]:::pj
mb["micronaut-base"]
ts["test-suites"]
mpb["multiplatform-base"]:::pj
lc["linter-convention"]
mpc["maven-publish-convention"]
msp["munchies-subproject"]
es["express-server"]:::pj
ml["micronaut-library"]:::pj
ms --> msp
es --> msp
mpb --> msp
ml --> msp
mpb --> mpc
mpb --> lc
es --> lc
kj --> lc
kj --> dk
mb --> ts
mpb --> dk
ms --> mpc
ml --> mpc
mb --> kj
ms --> mb
ml --> kj
classDef pj fill:#9956e0,stroke:#333,stroke-width:2px,color:#fff
Express Server Convention¶
Many of our subproject are developed in TypeScript, as such we needed to find a way to share the same functionalities and code between JVM, Multiplatform and TypeScript subprojects.
We did so through the express-server.gradle.kts, which is tasked with integrating Gradle's most used tasks (clean, build, run) into a npm-reliant subsystem;
this was done through a plugin which allows Gradle to run npm commands and a custom dependency (jsImplementation) between subproject that builds, archives and links JavaScript modules from Multiplaform subprojects.
We've decided to go along with these steps, so that during the development of TypeScript subprojects the library dependencies would align with the local version and as a result be up-to-date.
Micronaut Library Convention¶
Not every shared piece of code between our JVM services is a plain Kotlin class, some of it needs to be a real
Micronaut bean, picked up automatically by every service's dependency-injection context. A plain
kotlin-jvm module can't do that: Micronaut only turns a class into an injectable bean if it was compiled
with Micronaut's own KSP annotation processor, which a bare Kotlin module doesn't run.
This came up concretely while investigating why our Kubernetes readinessProbe, which checks whether a pod
receives traffic, couldn't be fully trusted: Micronaut's Kafka health indicator does a genuine broker
round-trip, but no equivalent exists for our MongoDB setup (the built-in one only ships for the reactive
driver, and our services use the synchronous one through Micronaut Data). Rather than copy-paste a fix into
user-service, order-service and restaurant-service individually, we wrote it once, as a real shared
Micronaut bean.
micronaut-library.gradle.kts
is the minimal convention that makes this possible: just enough Micronaut (KSP processing, allopen) for a
project's classes to become real beans, without the io.micronaut.application, Docker or AOT machinery a
full deployable service needs.
plugins {
id("kotlin-jvm")
id("org.jetbrains.kotlin.plugin.allopen")
id("com.google.devtools.ksp")
id("io.micronaut.library")
}
micronaut-commons is the one project
using it, currently holding a single class: a custom MongoHealthIndicator that pings MongoDB through the
synchronous client our repositories already use, so /health genuinely reflects whether a pod can reach its
database; that is verified live by watching /health flip to 503 when Mongo was stopped. Every
service using the micronaut-server convention pulls it in with a single line: