Module 3 ended on an honest note: the code for a complete service still does not exist. Before writing it, there is a decision to make that shapes everything else: what TechCorp's services will be built with. In a monolith that decision is made once and for all; with microservices it can be made per service, and that freedom is at once the biggest advantage and the biggest source of chaos. In this lesson we set the selection criteria, walk through the technology landscape by category, make TechCorp's concrete decisions (with Marta's two-databases rule as an example of "freedom with limits") and prepare the practical groundwork: the service template, the @techcorp/common-http library, the local development tools and the repository layout. Everything decided here will be used, as is, in the next four lessons.
Contents
- Selection criteria for a microservice
- Languages and frameworks
- Databases and cache
- Message brokers, configuration, containers, CI/CD, observability and testing
- Heterogeneity with judgment: polyglot, but with a short list
- The service template and the
@techcorp/common-httplibrary - Local development tools
- Monorepo or multirepo: organizing the code
- Selection criteria for a microservice
Choosing technology for a microservice is not the same as choosing it for a monolithic application. A monolith starts once a day and occupies one server; a microservice is replicated, restarts frequently (deployments, autoscaling, pod rescheduling) and coexists with dozens of neighbors in the same cluster. That changes the weight of each criterion:
| Criterion | What it measures | Why it weighs more with microservices |
|---|---|---|
| Maturity and ecosystem | Years of production use; libraries for HTTP, DB, AMQP, OpenTelemetry, testing | Every service needs every item on the list; a gap gets multiplied by the number of services |
| Team knowledge | How many people can write, review and operate code in that technology | With ~25 engineers, a service in a language only one person knows is a continuity risk |
| Performance | Requests per second per replica, p99 latency | It matters, but less than people think: 3,000 orders/day do not call for Go; neither do the ×20 peaks (01-05) |
| Startup time | Seconds from docker run to /health/ready |
With rolling updates and autoscaling (05-04, 06-04), a 40 s startup delays every deployment and every scale-out |
| Memory footprint | MB at rest per replica | Ten services × three replicas × 512 MB is an entire node; in Kubernetes memory is paid per replica |
| Container support | Image size, official images, behavior on SIGTERM | A process that ignores SIGTERM loses requests on every deployment (we will see it in 04-02) |
| Observability | Available, stable instrumentation for structured logs, Prometheus metrics and OpenTelemetry traces | Without it, debugging a flow that crosses five services (module 6) is impossible |
| Development speed | What a new endpoint or a new consumer costs | The incremental migration (01-05) will create seven services in a few months |
A methodological tip: score the criteria before looking at technologies. If the team first picks the technology it likes and then looks for criteria to justify it, the table is useless.
- Languages and frameworks
An overview of the usual options for HTTP + events services (indicative values for a small service with one endpoint and one consumer):
| Technology | Startup | Memory at rest | Performance | Microservices ecosystem | Learning curve for TechCorp's team |
|---|---|---|---|---|---|
| Node.js 20 + Express | < 1 s | ~40-60 MB | Medium-high (asynchronous I/O) | Excellent: pg, mongodb, amqplib, pino, OpenTelemetry, Jest |
None: it is the monolith's language |
| Node.js + Fastify | < 1 s | ~40-60 MB | High (2-3× Express in benchmarks) | Excellent; built-in JSON Schema validation | Low: same language, different API |
| Node.js + NestJS | 1-2 s | ~70-100 MB | Medium-high | Very good; opinionated (modules, DI, decorators) | Medium: TypeScript + its own structure |
| Java + Spring Boot | 5-15 s | ~250-400 MB | High | The most complete (Spring Cloud) | High: nobody at TechCorp writes Java |
| Java + Quarkus / Micronaut | < 1 s (native) / 2-3 s (JVM) | ~50-150 MB | High | Very good; designed for containers | High |
| Go (net/http, Gin, Echo) | < 100 ms | ~10-20 MB | Very high | Good; single binary, 10-20 MB images | Medium-high: two people on Platform know it |
| Python + FastAPI | 1-2 s | ~50-80 MB | Medium | Good; typing with Pydantic, automatic OpenAPI | Medium: the data team uses it |
| .NET 8 (Minimal APIs) | 1-2 s | ~60-100 MB | High | Very good | High: no experience in the company |
How to read the table:
- There is no "right" option: Spring Boot is an excellent choice in a Java shop, and a terrible one at TechCorp, where nobody knows it and the 10 s startup would complicate autoscaling.
- Node.js and Go are the two usual extremes of "lightweight": Node for productivity and ecosystem, Go for footprint and performance. Many companies use both: Node for business services, Go for infrastructure pieces or high-traffic components.
- Express vs. Fastify vs. NestJS is a minor decision compared with the language choice. Express is the best known and the one the monolith already uses; Fastify is faster and ships validation; NestJS imposes structure (useful in large teams, a burden in small ones). Since performance is not TechCorp's bottleneck and the monolith is already Express, the cost of switching does not pay off today.
TechCorp's decision (Marta and the four teams, architecture meeting): Node.js 20 LTS + Express in JavaScript for every service in the first wave. It is put in writing that Go is the second approved technology for services with performance or footprint requirements (candidate: a future catalog search service), and that TypeScript will be evaluated once the first service is in production. The rationale in one sentence: "the project's risk lies in the distributed architecture, not in the language; let's not add a second learning curve."
- Databases and cache
Here module 2 already did the heavy lifting (02-04): one database per service, PostgreSQL for orders, customers, payments and inventory; MongoDB for the catalog. What this lesson adds is the general criterion and the role of the cache:
| Technology | Model | When to choose it in a microservice | When not to |
|---|---|---|---|
| PostgreSQL | Relational, ACID transactions, JSONB | Aggregates with invariants (Order, Reservation, Payment), need for a local transaction (outbox, processed_events), queries with combined filters |
Documents with a highly variable structure and no relationships (possible with JSONB, but MongoDB is more natural) |
| MongoDB | Document store, flexible schema | Self-contained, heterogeneous documents (product sheets with different attributes per category), reads by id or by a few indexes |
Frequent cross-collection transactions, strong invariants (multi-document transactions exist, but they are a sign the model is not document-oriented) |
| Redis | In-memory key-value, TTL, data structures (lists, sets, sorted sets) | Cache for expensive reads (the response of GET /v1/products?ids=), counters, gateway rate limiting (03-04), sessions |
As the primary database of a business aggregate: it is memory; persistence is optional and the guarantees are different |
| Others (Cassandra, DynamoDB, Elasticsearch...) | Columnar, managed key-value, search | Specific volumes or use cases (full-text search, time series) | Before you have the problem they solve |
One clarification about Redis: a cache is not "another database" for the purposes of Marta's rule ("at most two database technologies," 02-04) because it does not hold the truth of any aggregate; if Redis is flushed, the system keeps working, only slower. Even so, TechCorp does not introduce it yet: Cache-Control: max-age=30 in Catalog (03-01) covers the current need. It is noted as a candidate for 06-04 (performance).
- Message brokers, configuration, containers, CI/CD, observability and testing
The remaining categories of the stack each have their own lesson in this course; here we give only the map and the decision, so you can see the whole picture at once.
Message brokers (details in 03-02):
| Broker | Model | Strength | When to choose it |
|---|---|---|---|
| RabbitMQ | AMQP queues, exchanges, flexible routing, native DLQ | Clear queue semantics, mature tooling, easy to operate at medium scale | Business events and commands, volumes from thousands to hundreds of thousands of messages/day |
| Apache Kafka | Partitioned, persistent log; consumers that re-read | Massive volume, replay, streaming | Millions of events/day, real-time analytics, event sourcing |
| NATS / JetStream | Very lightweight, pub-sub, queues with JetStream | Minimal latency, simple operation | Internal communication in large clusters, IoT |
Decision: RabbitMQ, already made in 03-02, because of the volume (3,000 orders/day), the native DLQ and the one-queue-per-consumer topology we defined.
Configuration management (details in 04-03): environment variables as the universal interface; Kubernetes ConfigMaps and Secrets as the source in production; Consul KV / Vault / Spring Cloud Config as centralized options when there are dozens of services with shared configuration. TechCorp: environment variables, period.
Containers and orchestration (module 5): Docker to build images; Kubernetes to run them in production; alternatives such as Nomad, ECS or Cloud Run make sense in specific contexts. TechCorp: Docker + Kubernetes; locally, Docker Compose. Every service in this module will be containerized in 05-01, which is why we write it from the start so that it boots fast, reads its configuration from the environment and dies cleanly on SIGTERM.
CI/CD (05-03): GitHub Actions (TechCorp's code lives on GitHub), GitLab CI and Jenkins are equivalent in capability; the choice is dictated by where the code lives.
Observability (module 6): logs with pino in JSON → Loki; metrics with prom-client → Prometheus + Grafana; traces with OpenTelemetry → Jaeger. The classic alternative is ELK (Elasticsearch, Logstash, Kibana). In this module we will only use pino as a minimal logger.
Testing (04-05): Jest or Vitest as the runner, Supertest to test Express without opening a port, Testcontainers to spin up real PostgreSQL/RabbitMQ in integration tests, Pact for consumer-provider contracts.
- Heterogeneity with judgment: polyglot, but with a short list
One of the selling points of microservices (01-02) is technological freedom: each team picks what is best for its problem. Experience says that freedom, without limits, produces the following within two years: seven languages, four databases, two brokers, and a critical service in Elixir nobody dares to touch because the person who wrote it left. The cost is not in writing, but in operating: every technology needs base images, CI templates, dashboards, alerts, security guides and on-call people who understand it.
The mature answer is not to forbid heterogeneity, but to govern it:
flowchart LR
A[Team wants to use<br/>technology X] --> B{Is it on the<br/>approved list?}
B -- Yes --> C[Go ahead: template,<br/>CI and observability already exist]
B -- No --> D{Does it solve a problem<br/>the list does not?}
D -- No --> E[Use the approved one]
D -- Yes --> F[Proposal to the architecture<br/>committee: scoped pilot]
F --> G{Does the pilot justify<br/>the operational cost?}
G -- Yes --> H[Added to the list:<br/>Platform builds template and support]
G -- No --> E
TechCorp's approved short list, exactly as the Platform team publishes it in its repository:
| Category | Approved today | Approved with justification | Out |
|---|---|---|---|
| Language/framework | Node.js 20 LTS + Express (JavaScript) | Go (high performance/low footprint); TypeScript (after the first service in production) | Anything else without going through the committee |
| Database | PostgreSQL 16 | MongoDB 7 (Catalog only, for now) | A third technology: Marta's rule, at most two |
| Cache | — | Redis (when 06-04 justifies it) | — |
| Broker | RabbitMQ | — | Kafka as long as the volume does not require it |
| Containers | Docker + Kubernetes | — | — |
| Observability | pino + Prometheus/Grafana/Loki + OpenTelemetry/Jaeger | — | ELK in parallel |
Notice the detail in Marta's rule: it is not "PostgreSQL and nothing else," it is "two technologies," which made room for MongoDB where its model fits (02-04) while closing the door to a third. That is the spirit of the short list: real freedom within a perimeter Platform can sustain.
- The service template and the
@techcorp/common-http library
@techcorp/common-http libraryWith seven services on the same technology, whatever repeats is best solved once, and there are two different mechanisms for that which must not be confused:
6.1 The service template (archetype)
It is a sample repository (techcorp/node-service-template) that is copied when creating a new service. It contains the folder structure, the package.json with the agreed dependencies and scripts, the Dockerfile (05-01), the CI workflow (05-03), the eslint configuration, a .env.example and a sample service with /health/live and /health/ready that boots on the first try. A template is copied and then each service evolves on its own: there is no coupling, which is why it can carry opinions (folder structure, script names). In 04-02 we will build catalog-service from scratch precisely to understand what the template would give us for free.
6.2 The @techcorp/common-http library
It is a versioned npm package, published in TechCorp's private registry, that every service declares as a dependency ("@techcorp/common-http": "^1.2.0") and upgrades when it suits it. We have been naming it since 02-02; now we pin down its contents:
| Exports | What it does | Where it was defined |
|---|---|---|
createLogger({ service }) |
A JSON pino logger with the common fields (service, level, time, requestId) |
Here; full format in 06-01 |
requestIdMiddleware() |
Reads X-Request-Id or generates one (req-<ulid>), puts it in req.id, in the response and in the request logger |
03-01 (correlation) |
sendProblem(res, req, problem) |
RFC 7807 application/problem+json format with code and instance |
03-01 |
errorMiddleware({ logger }) |
Last Express middleware: turns exceptions into problem+json (generic 500 or the business code) |
04-02 |
createHealthRoutes({ checks }) |
/health/live and /health/ready with a 1 s timeout per dependency and 503 during shutdown |
03-05 |
BusinessError(code, message, status) |
Base class for domain errors (CUSTOMER_NOT_FOUND, PRODUCT_UNAVAILABLE...) that the middleware knows how to translate |
04-02 |
messaging/topology (connect, declareConsumerQueue) and messaging/publisher (buildEnvelope, publishEvent) |
RabbitMQ topology and standard envelope | 03-02 |
And, just as important as what it contains, what it does not contain:
processOnceis not in the library. It depends on each service's database (processed_eventstable in PostgreSQL in Orders; in Catalog, if it ever consumes events, it would be a MongoDB collection). Putting it in the library would force the library to knowpgandmongodb, and would drag every service onto the same driver version. Each service implements it (it is 15 lines) on top of its own DB.- No business model. No
Order, noProduct, no business validations. A library with shared domain logic is the distributed monolith of 02-01: a change toOrderwould force everyone to redeploy. - No client for another service.
catalogClient.jslives in Orders, not in the library: it is Orders that decides what it needs from Catalog and with what timeout. - No concrete configuration. The library does not know Catalog's URL; it receives values.
The rule for deciding: if changing this piece would require several services to be redeployed at the same time, it does not belong in the library. Everything in @techcorp/common-http is technical, stable and optional to upgrade.
- Local development tools
A TechCorp developer needs to be able to start their service on their laptop, with its real dependencies (a database, RabbitMQ) and without spinning up the other six services. The agreed toolbox:
| Tool | What for | Note |
|---|---|---|
Node.js 20 LTS (via nvm or fnm) |
Run the services; same major version as the production image | .nvmrc with 20 in every repository |
| npm | Dependencies and scripts (npm run dev, npm test) |
npm ci is used in CI to honor package-lock.json |
| nodemon | Restart the service on file save during development | devDependencies only; in production, node src/server.js |
| Docker Desktop / Podman | Spin up local PostgreSQL, MongoDB and RabbitMQ with a docker run or a docker compose up |
The service runs with Node on the laptop; only the dependencies run in containers (05-01 takes the next step) |
| curl / HTTPie / Postman | Try endpoints by hand | In this course we use curl so the examples are copy-pasteable |
| VS Code + REST Client extension | .http files versioned alongside the service with the sample requests |
Replaces Postman collections kept outside the repository |
RabbitMQ Management (http://localhost:15672) |
Inspect queues, messages, DLQ | Ships with the rabbitmq:3-management image |
| psql / mongosh | Inspect data | IDE extensions work too |
Example of local dependencies, exactly as each lesson in this module will start them (one line each; the full docker compose comes in 05-01):
# MongoDB for catalog-service (04-02)
docker run -d --name mongo-catalog -p 27017:27017 mongo:7
# PostgreSQL for orders-service (04-04). Fake password, development only.
docker run -d --name pg-orders -p 5432:5432 -e POSTGRES_USER=svc_orders \
-e POSTGRES_PASSWORD=dev-orders -e POSTGRES_DB=orders postgres:16
# RabbitMQ with the management console (04-04)
docker run -d --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:3-managementWith this, the ORDERS_DB_URL variable from 03-05 is postgres://svc_orders:dev-orders@localhost:5432/orders locally and RABBITMQ_URL is amqp://localhost:5672. Same variable names, different values per environment: exactly the mechanism 04-03 will formalize.
- Monorepo or multirepo: organizing the code
Last decision before writing code: where each service lives.
| Aspect | Monorepo (one repository for everything) | Multirepo (one repository per service) |
|---|---|---|
| Changes touching several services | A single commit and a single PR | Several coordinated PRs |
| Team autonomy | Lower: shared permissions, CI and conventions | Maximum: each team decides its CI, its pace, its reviewers |
| Independent deployment | Possible, but you must prevent a change in A from triggering B's pipeline (path filters) | Natural: each repository has its own pipeline |
| Shared libraries | Imported by path; always on the latest version | Published to a registry and each service picks its version |
| Tooling required | Nx, Turborepo, Bazel or similar beyond a certain size | Nothing special; a template, though, so as not to diverge |
| Typical risk | That the convenience of touching everything at once recreates the monolith | That utilities get duplicated and conventions drift |
| Examples | Google, Meta (with in-house tooling) | Most mid-sized companies |
Both models work; what matters is that the choice reinforces, rather than contradicts, the architecture. For TechCorp the key is the per-team deployment autonomy we defined in 02-02, and the size (seven services, four teams) does not justify setting up Nx or Bazel.
TechCorp's decision:
- One repository per service:
techcorp/catalog-service,techcorp/orders-service,techcorp/inventory-service, etc. Each with itsDockerfile, its CI workflow and its owning team. - One platform repository (
techcorp/platform): Kubernetes manifests, gateway definition (03-04), dashboards, the service template and the approved short list from section 5. - One repository for
@techcorp/common-http, published as a package in the private npm registry (GitHub Packages), with semantic versioning: services upgrade when they want to, not when the library changes. - Contracts (the OpenAPI and AsyncAPI from 03-06) live in the provider service's repository, under
contracts/, so they change in the same PR as the code.
Resulting structure, in summary:
techcorp/ ├── platform/ # Platform team: k8s/, gateway/, node-service-template/, approved-technologies.md ├── common-http/ # @techcorp/common-http (private npm package) ├── catalog-service/ # Shopping Experience team (04-02) ├── customers-service/ # Shopping Experience team ├── orders-service/ # Orders team (Luis) (04-04) ├── inventory-service/ # Orders team (Luis) ├── payments-service/ # Payments & Communications team ├── notifications-service/ # Payments & Communications team └── techcorp-shop/ # the monolith, gradually being emptied
Common Mistakes and Tips
- Choosing technology by fashion or by résumé. "We want Go because it's what everyone uses" or "Kafka because Netflix uses it." Apply the table from section 1 with TechCorp's real numbers: 3,000 orders/day do not need Kafka.
- Confusing freedom with absence of rules. The short list is not bureaucracy; it is what lets Platform provide real support. Write it down, publish it and evolve it through pilots.
- Putting business logic in the shared library. Every
OrderorProductin@techcorp/common-httpis a thread that stitches the services back together. Only technical, stable, versioned code. - Putting things that depend on the DB in the library (such as
processOnce). The library would end up depending onpgandmongodbat the same time. - Copying the template and never looking at it again. The template evolves (new linter, new log field). A quarterly review of which improvements to apply to existing services, without forcing them, is worthwhile.
- Running locally with different versions than production. Node 18 on the laptop and Node 20 in the image hides differences (for example, native
fetch)..nvmrcand the same major version everywhere. - A monorepo without tooling or a multirepo without a template. The first recreates the monolith; the second produces seven different ways of doing the same thing.
Exercises
Exercise 1. The Payments & Communications team proposes writing notifications-service in Python with FastAPI because "it only sends emails and Python has good templating libraries." Apply the flowchart from section 5 and the criteria from section 1, and write the architecture committee's answer in five lines.
Exercise 2. For each of these pieces, state whether it belongs in @techcorp/common-http, in the service template, or in the service itself, and why: (a) the transition function of the order state machine; (b) the middleware that generates X-Request-Id; (c) the .eslintrc file; (d) the productTranslator; (e) declareConsumerQueue; (f) the Dockerfile.
Exercise 3. Marta asks whether, to keep things simple, a single repository with all the services in folders would not be better. List two real advantages TechCorp would gain and two concrete risks for the architecture from 02-02, and propose a condition under which the answer would be "yes."
Solutions
Solution 1. Python is not on the approved list, so the question is whether it solves something Node does not. It does not: Node has equivalent email templating libraries (for example nodemailer + template engines), and Notifications' problem is neither performance nor footprint. The cost would be a second base image, a second CI template, different OpenTelemetry instrumentation and a service only two people could maintain. Committee answer: "Rejected for this service. Notifications is built in Node.js 20 + Express with the standard template. If the team identifies a specific need Node does not cover (for example, PDF generation with a specific library), it may propose a scoped pilot with that justification."
Solution 2. (a) transition: in the Orders service; it is business logic of the aggregate; sharing it would couple. (b) X-Request-Id middleware: in the library; it is technical, identical for everyone and stable. (c) .eslintrc: in the template; it is copied and each service can tweak it without affecting anyone. (d) productTranslator: in the Orders service; it is its ACL, it expresses what Orders understands about Catalog. (e) declareConsumerQueue: in the library (messaging/topology); it is technical and encapsulates the DLQ convention from 03-02. (f) Dockerfile: in the template; each service copies and adapts it (for example, Catalog does not need the PostgreSQL client).
Solution 3. Advantages: cross-cutting changes (for example, bumping the @techcorp/common-http version in every service) in a single PR; a single linter, CI and review configuration. Risks: that one deployment drags another along (a PR touching Orders and Inventory "while we're at it" recreates deployment coupling); and that relative-path imports between services (../catalog-service/src/...) slip in unnoticed, breaking database-per-service and contracts through the back door. The answer would be "yes" if TechCorp adopted a monorepo tool (Nx/Turborepo) with path-filtered pipelines and dependency rules that forbid importing across services; with seven services and four teams, that effort does not pay off today.
Conclusion
We have turned the technological freedom of microservices into concrete, governed decisions: the criteria that weigh in a replicated, ephemeral service (startup, memory, containers, observability, team knowledge) above raw performance; the landscape by category; TechCorp's approved short list (Node.js 20 + Express in every service, PostgreSQL + MongoDB under Marta's two-databases rule, RabbitMQ, Docker/Kubernetes, pino/Prometheus/OpenTelemetry, Jest/Supertest/Testcontainers/Pact, with Go as a justified second option); the difference between the template that gets copied and the @techcorp/common-http library that gets versioned (and the golden rule: nothing business-related and nothing DB-dependent in the library); the local development tools; and the layout of one repository per service plus the platform one.
With the toolbox closed, it is time to open it. In the next lesson we build catalog-service from scratch, the first service extracted from the monolith: npm init, the layered structure, the separation between app and server, the GET /v1/products endpoints from the 03-01 contract with MongoDB behind them, RFC 7807 error handling, health and graceful shutdown, until it answers curl on port 3001.
Microservices Course
Module 1: Introduction to Microservices
- Basic Concepts of Microservices
- Advantages and Disadvantages of Microservices
- Comparison with the Monolithic Architecture
- When to Adopt Microservices: Decision Criteria
- The Course Case Study: TechCorp's Online Store
Module 2: Microservice Design
- Microservice Design Principles
- Decomposing Monolithic Applications
- Defining Bounded Contexts
- Data Management: One Database per Service
- Distributed Consistency: Sagas, CQRS and Event Sourcing
Module 3: Communication between Microservices
- RESTful APIs
- Asynchronous Messaging
- Communication Protocols: gRPC, GraphQL
- API Gateway and Backend for Frontend
- Service Discovery and Load Balancing
- API Contracts and Versioning
Module 4: Implementing Microservices
- Choosing Technologies and Tools
- Building a Simple Microservice
- Configuration Management
- Hands-On Integration: Consuming APIs and Publishing Events
- Testing Microservices: Unit, Integration and Contract Tests
Module 5: Deployment and Orchestration
- Containers and Docker
- Orchestration with Kubernetes
- CI/CD for Microservices
- Deployment Strategies: Rolling, Blue-Green and Canary
- Service Mesh: Istio and Linkerd
Module 6: Monitoring and Maintenance
- Monitoring and Logging
- Distributed Tracing with OpenTelemetry
- Error Handling and Recovery
- Scalability and Performance
- SLOs, Alerts and Incident Management
Module 7: Security in Microservices
- Authentication and Authorization
- Communication Security
- Security Practices
- Container and Kubernetes Security
