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

  1. Selection criteria for a microservice
  2. Languages and frameworks
  3. Databases and cache
  4. Message brokers, configuration, containers, CI/CD, observability and testing
  5. Heterogeneity with judgment: polyglot, but with a short list
  6. The service template and the @techcorp/common-http library
  7. Local development tools
  8. Monorepo or multirepo: organizing the code

  1. 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.

  1. 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."

  1. 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).

  1. 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.

  1. 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.

  1. The service template and the @techcorp/common-http library

With 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:

  • processOnce is not in the library. It depends on each service's database (processed_events table 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 know pg and mongodb, 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, no Product, no business validations. A library with shared domain logic is the distributed monolith of 02-01: a change to Order would force everyone to redeploy.
  • No client for another service. catalogClient.js lives 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.

  1. 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-management

With 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.

  1. 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 its Dockerfile, 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 Order or Product in @techcorp/common-http is 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 on pg and mongodb at 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). .nvmrc and 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

Module 2: Microservice Design

Module 3: Communication between Microservices

Module 4: Implementing Microservices

Module 5: Deployment and Orchestration

Module 6: Monitoring and Maintenance

Module 7: Security in Microservices

Module 8: Case Studies and Practical Examples

© Copyright 2026. All rights reserved