The last document of the course materials is not a lecture but a project brief: Traccia Progetto — ChatFlow Middleware. It is written in the language of a client rather than of an engineer, it is two pages long, and it is exactly the kind of artefact that Chapter 1 described when it introduced the project work option — a set of requirements provided by a commissioner, to be turned into a domain model and a delivery pipeline by the students.
The original brief is in Italian. This chapter reports its content in English, keeping verbatim the strings that are data rather than prose — the command /riepilogo 2025, the tenant name ComuneXYZ, the JSON keys ore_totali and assente, and the reply text — because an identifier that gets translated stops being an identifier. Sections 1 to 7 report the brief; section 8 is explicitly an exercise: it applies the criteria of Chapters 2 to 6 to the brief, and says so wherever it goes beyond what the document states.
Build a middleware system that:
Four verbs, four responsibilities. Notice that the middleware is defined entirely by what passes through it: it owns no business data of its own, it holds no user accounts, and its value is the translation and the routing. That observation will do a lot of work in section 8.
The brief gives the architecture as a single line:
[Telegram/WhatsApp] ⇄ [Middleware BridgeBot] ⇄ [Gestionale REST Multi-Tenant]
That is: messaging channels ⇔ the BridgeBot middleware ⇔ a multi-tenant REST management system. Both arrows are bidirectional, which is the whole difficulty: the response must find its way back not merely to "the user", but to the correct channel of the original sender.
| # | Component | Requirements |
|---|---|---|
| 1 | Handling input from messaging channels | Integration with the Telegram Bot API; integration with the WhatsApp Cloud API (or a simulation of it) |
| 2 | Definition and parsing of a meta-language | An intermediate language for commands; a parser that produces standardised JSON objects |
| 3 | Routing requests to the management backends | Each tenant has an API key; requests are forwarded over REST with an Authorization header |
| 4 | Returning the response to the user | Formatting of the response; delivery on the originating channel |
| 5 | Configuration and security | Mapping chatId → tenant / API key; safe logging and rate limiting |
Component 2 is the one worth pausing on, because the course has a whole chapter about it. An intermediate language for commands with a parser producing standardised JSON objects is, in the vocabulary of Chapter 5, an external DSL: a custom syntax, hence a custom parser, whose output is a model. The choice the brief leaves open — how rich that language should be — is precisely the DSL-versus-GPL trade-off: few and static constructs, a clear domain boundary, a small and reachable user base, fast-paced evolution.
Component 5 is what makes the system multi-tenant in practice. A chatId arrives from a messaging platform and means nothing to the backend; the mapping turns it into a tenant identity and the credential to act on that tenant's behalf. Everything else in the flow depends on that lookup being correct.
Four constraints, stated flatly:
Read as a set, they describe a deliberate narrowing of responsibility. The third constraint is the strongest: by refusing to authenticate end users, the middleware also refuses to hold user credentials, which removes an entire class of risk from the system — and simultaneously shifts weight onto the chatId mapping, since that mapping becomes the only thing deciding on whose behalf a request is made.
Two of the four constraints concern logs, not features: nothing sensitive in clear, and debug logging with obfuscated data. Logs are the most frequently forgotten copy of the data — they are written by everybody, read by anybody, and rarely reviewed. The brief pairs them with rate limiting in component 5, which is the other operational concern that only shows up under real traffic.
| Deliverable | Note |
|---|---|
| Documented source code | Documentation is part of the delivery, not an afterthought |
| Configuration file (JSON/YAML) | The chatId-to-tenant mapping and the rest of the configuration live outside the code |
| Startup script or Dockerfile | The deployment story is an explicit deliverable |
| Technical README | The entry point for whoever inherits the system |
| Optional extension: a web UI for configuration | Listed as optional here, and again among the extensions |
Two of the five items are about running the thing rather than writing it. That is the same emphasis the course places everywhere: a configuration file separated from the code is what allowed an MLflow Project to be re-run with different parameters in Chapter 6, and a Dockerfile is the deploy automation via containerization that Chapter 1 listed among the exam requirements.
The brief closes its functional part with one worked example. It is six lines long and it exercises every component listed in section 3.
User (Telegram): /riepilogo 2025
→ BridgeBot receives the message with chatId: 654321
→ maps chatId to tenant 'ComuneXYZ'
→ retrieves the API key and sends the REST request
→ receives JSON: { 'ore_totali': 124, 'assente': false }
→ replies to the user:
'Nel 2025 hai totalizzato 124 ore. Nessuna assenza registrata.'
In English, the command means summary 2025, the JSON keys mean total hours and absent, and the reply reads "In 2025 you accumulated 124 hours. No absence recorded." The tenant name ComuneXYZ stands for a municipality.
Follow the responsibilities through the six lines: parsing (a command with one argument becomes a structured request), identification (a channel-level chatId becomes a tenant), authorisation (the tenant's API key goes into the Authorization header), invocation (REST), formatting (a JSON payload becomes a sentence a human wants to read), and delivery (back on Telegram, because that is where the message came from). The response is not the JSON: it is prose generated from the JSON, which is why "formatting the response" is a named component and not an implementation detail.
The brief suggests, without prescribing:
The last suggestion in the third tab is the one with teeth. Modularity, to divide the work among students is the brief asking for a decomposition whose seams match the team — which is the same argument that Chapter 4 made about contexts, where the boundary is organizational as well as technical, and where the best practice reads one responsible person or team for each context.
Everything up to here reported the brief. What follows applies the criteria taught in Chapters 1 to 6 to that text. The brief itself does not mention DDD, DSLs, hexagonal architecture or CI/CD: the mapping below is the work the students are expected to do, and it is presented as one defensible reading, not as the answer.
The brief hands over a vocabulary with almost no noise: message, channel, chatId, meta-command, tenant, API key, request, response. Following Chapter 2, these are the words that should appear in the code, and the workflow prescribes recording their meaning in a glossary rather than assuming it. Two of them repay the discipline immediately:
Component 5 of the brief exists precisely because those two words live in different contexts, and the mapping is the point of contact between them — the sort of junction a context map is meant to record.
Applying the decision procedure of Chapter 3 — identity or interchangeability, thing or capability — gives a plausible first classification:
| Concept from the brief | Candidate block | Why |
|---|---|---|
ChatId, ApiKey, TenantId | Value objects | They are their content: two chat ids with the same value are the same chat id. Immutability also helps with the "nothing sensitive in clear" constraint, since a value object is easy to wrap in a type that controls its own rendering |
Tenant | Entity | It has an identity that survives changes to its attributes (name, key rotation) |
MetaCommand (the parsed request) | Value object | A standardised JSON object describing one request; two identical requests are indistinguishable |
The chatId→tenant/API-key mapping | Repository | It mediates persistent storage and retrieval, and its implementation may be the JSON/YAML configuration file or the optional SQLite database — a swap the rest of the system should not notice |
| Parsing, routing, replying | Services | Stateless capabilities that wire the other objects together for one use case each |
The repository row is the interesting one, because the brief itself offers two storage options — a configuration file, or an optional SQLite database. Chapter 4 has a name for an interface designed so that both fit behind it without leaking: an anti-corruption layer, and the third DDD exercise (the CSV one) is exactly the drill for it, down to the test that must pass unchanged against two implementations.
Component 2 asks for an intermediate language for commands plus a parser producing standardised JSON objects. In the vocabulary of Chapter 5 this is an external DSL: a custom concrete syntax with a custom parser, whose output is a model instance. The design questions the chapter supplies transfer directly:
/riepilogo 3025 parses. Whether a year in the future is meaningful is a validation rule — the same distinction that made ClockTime need a validator in the Sheduler language.| Exam requirement (Ch. 1) | What the brief already provides | What the team must add |
|---|---|---|
| Domain-driven design | A domain description in the client's own words, and a vocabulary | Glossary, context map, model named after the language, building-block choices |
| Clear process and DevOps practices | Documented source and a technical README among the deliverables | A visible workflow in the repository |
| Full-scale automation, including CI/CD | — | The whole pipeline: this is not requested by the brief and is required by the exam |
| Deploy automation via containerization | A startup script or Dockerfile is an expected deliverable | Wiring the image into the pipeline |
| Two or more target platforms | The brief suggests either a Python stack or a Node.js stack | Note the criterion: CPython and NodeJS are different runtimes, so a system whose components genuinely run on both satisfies the rule — choosing one stack for everything does not |
A brief like this one is the natural object of the oral discussion, because every course topic has a foothold in it. Be ready to say: which words you put in the glossary and why; which concepts became entities rather than value objects; where you drew the context boundary between the channel side and the backend side; why the command language is a DSL and where its validation rules live; how the chatId-to-tenant mapping is stored and how you kept that decision swappable; and how the four security constraints are enforced by construction rather than by discipline.
Receive messages from messaging systems (WhatsApp and Telegram); translate the messages into an intermediate language of meta-commands; forward the requests to a multi-tenant REST management backend through secure APIs; and return the response to the original sender, on the correct channel.
(1) Handling input from the messaging channels (Telegram Bot API, WhatsApp Cloud API or a simulation); (2) definition and parsing of the meta-language, with a parser producing standardised JSON objects; (3) routing the requests to the management backends, with one API key per tenant and an Authorization header; (4) returning the response, formatted, on the originating channel; (5) configuration and security, with the chatId-to-tenant/API-key mapping, safe logging and rate limiting.
No sensitive data stored in clear; a dedicated API key per tenant; the middleware does not handle end-user authentication; logging only for debug, with obfuscated data. The third is the most consequential: it removes user credentials from the system entirely, and in exchange makes the chatId mapping the sole determinant of whose data is being accessed.
chatId-to-tenant mapping the heart of the system?Because chatId is a channel-side identifier that means nothing to the backend, while tenant is a backend-side identity with an API key attached. The mapping is the point of contact between two contexts, and it is what decides on whose behalf every request is made — especially given that the middleware performs no end-user authentication of its own.
The user sends /riepilogo 2025 on Telegram; BridgeBot receives the message with chatId: 654321; it maps that id to the tenant ComuneXYZ; it retrieves the API key and sends the REST request; it receives { 'ore_totali': 124, 'assente': false }; and it replies to the user with the sentence "Nel 2025 hai totalizzato 124 ore. Nessuna assenza registrata."
Because the deliverable to the user is prose, not JSON. The backend answers with a data structure; the user receives a sentence. That transformation is a named responsibility of the middleware, and it is also where the optional extension "replies in multiple languages" would attach.
Documented source code; a configuration file in JSON or YAML; a startup script or Dockerfile; a technical README; and, optionally, a web UI for configuration.
An external DSL: a fully custom syntax, hence requiring a custom parser, producing a model (the standardised JSON objects). It follows that its constructs should be few and static, that it need not be Turing-complete, and that parsing and validation are different jobs — a syntactically valid command may still be meaningless, and rejecting it is the validator's task.
The mapping and the logs may live in the JSON/YAML configuration file that is an expected deliverable, or in an optional SQLite database. Since the brief itself offers two, the design should hide the choice behind an interface — a repository acting as an anti-corruption layer — so that switching does not perturb the rest of the model.
No. It supplies a real domain to model, and a Dockerfile is already among its deliverables, but the brief says nothing about continuous integration and delivery or about a visible development process — those must be added. On platforms, note that the brief suggests either a Python stack or a Node.js stack: since CPython and NodeJS are different runtimes, genuinely targeting both satisfies the two-platform rule, while picking one stack for everything does not.
It lists modularity, to divide the work among students among the implementation suggestions. That makes the decomposition an organizational matter as much as a technical one — exactly the third perspective from which Chapter 4 says a context boundary is real, and the reason for the best practice one responsible person or team for each context.
A web UI for tenant management; support for voice messages with transcription; Telegram interactive buttons; and replies in multiple languages. Voice-to-transcription changes the input pipeline most: it inserts a new, fallible stage before parsing, and produces text that no longer necessarily conforms to the command syntax.