Skip to content
tridungngPublic

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

105 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

UberLite MVP

Minimal scaffold for the UberLite MVP implementing the Designing UberLite: a Ride Aggregator Service paper (Prasaad & Vikström, UW CSE552, Fall 2019).

Stack: Java 25, Spring Boot 4.x, Maven 3.9+, Docker Compose

Architecture Overview

See ARCHITECTURE.md for the complete service decomposition, data models, and API contracts. UberLite is a microservices project with:

  • 15 Maven modules — 9 core marketplace services, 3 background analytics services, and 3 infrastructure modules (discovery-server, api-gateway, common)
  • Postgres for trip state machine and static reference data
  • Redis for driver locations and surge multipliers (real-time, low latency)
  • Kafka for domain event triggers (trip state transitions propagate to analytics services)
  • Eureka for service discovery
  • Spring Cloud Gateway as API entry point
  • Micrometer + Zipkin for distributed tracing across every hop

Getting Started

From a clean clone to a completed trip in three commands.

Prerequisites

Tool Version Notes
JDK 25 java -version must report 25; the build sets --release 25
Maven 3.9+ or use the bundled ./mvnw
Docker with Compose v2 docker compose version — v2 syntax, not the old docker-compose binary
curl, jq any only needed by scripts/demo.sh (brew install jq)

You need roughly 8 GB of RAM free for Docker. The stack is 23 containers: 14 JVMs, six Postgres instances, Kafka (single-node KRaft — no ZooKeeper), Redis and Zipkin.

1. Clone and build

git clone <repo-url> UberLite
cd UberLite
./mvnw clean install

The Docker images build the code again inside the build stage, so this step is not strictly required to run the stack — but it fails fast, and much faster, if something is broken.

2. Bring the whole stack up

docker compose --profile all up --build

First run takes a while: every image compiles the Maven reactor. Later runs reuse the layer cache and a shared ~/.m2 build cache, so a one-line code change rebuilds one service in seconds.

Services start in parallel, not in a dependency chain — no service waits for another service (see Independent deployability). Watch progress with:

docker compose ps           # STATUS "healthy" = that JVM is serving
./scripts/smoke.sh          # one line per service: LIVE / READY / HEALTH

2b. Or bring up only what you need

The default (no profile) is the platform on its own, which is the fast inner loop:

docker compose up -d                                  # Eureka, gateway, Kafka, Redis, Zipkin, DBs
docker compose up -d trip-service                     # + one service and its database
docker compose up -d --no-deps --build trip-service   # rebuild just that one, leave the rest alone
docker compose --profile core up -d                   # the nine marketplace services
docker compose --profile analytics up -d              # the three background analytics services

Any service can run alone. Calls to peers that are not up fail fast with No instances available for <service> instead of hanging or preventing startup — see Independent deployability.

3. Run the end-to-end demo

In a second terminal:

./scripts/demo.sh

It drives the full happy path through the API gateway — puts two drivers online, creates and prices a trip, matches a driver, walks the trip to PAID, then checks that the Kafka-driven analytics services saw it. Every request and response is printed. It exits non-zero on the first unexpected status code, so it works as a smoke test in CI too.

Useful overrides:

BASE_URL=http://localhost:8083 ./scripts/demo.sh   # bypass the gateway, hit trip-service directly
READY_TIMEOUT_SECONDS=600 ./scripts/demo.sh        # slow machine / cold image cache

4. Where to look

What Where Shows
Eureka dashboard http://localhost:8761 every registered instance; all 13 clients should be listed
Zipkin UI http://localhost:9411 distributed traces — search by service trip-service
Aggregate health http://localhost:8080/health/aggregate one JSON document with the health of every registered service; 200 only when all are UP
Per-service health http://localhost:<port>/actuator/health see the port table below
Per-service metrics http://localhost:<port>/actuator/prometheus Micrometer metrics
API gateway http://localhost:8080 single entry point; routes listed in api-gateway/application.yml

To see the fan-out the demo talks about, open Zipkin, pick trip-service and look at the trace for POST /trips. One trace spans trip-service → price-estimation-service → route-service, time-estimation-service, surge-pricing-service, tax-tolls-service and discounts-promotions-service. scripts/demo.sh prints a direct link to its own trace when it finishes.

5. Shut down

docker compose down          # stop everything, keep the databases
docker compose down -v       # also drop the Postgres volumes for a truly clean slate

Running a single service outside Docker

Every module is independently runnable. Start the platform, then the one service:

docker compose up -d                          # Eureka, gateway, Kafka, Redis, Zipkin, databases
./mvnw -pl route-service spring-boot:run

Without the docker profile a service defaults to localhost for Eureka, Redis, Kafka and its database, and the host ports published by Compose match those defaults — so a locally run service drops into a partially containerised stack without extra configuration.

Independent deployability

Every service is a unit of deployment on its own: its own image, its own database, its own lifecycle. Three rules keep that true, and together they are why a failed docker compose up now names the service that is actually broken.

1. No service-to-service depends_on, and no service_healthy gates. Every dependency in docker-compose.yml is service_started. The Kafka and Eureka clients reconnect on their own, so they need no gate at all. A database is a real startup dependency - Hibernate validates the schema during context refresh - and that is handled by the restart policy rather than by Compose: the container exits, is restarted, and succeeds once Postgres accepts connections.

Health gates were removed because they are not portable. Podman does not execute Compose-declared health checks, so condition: service_healthy never opens there and the dependent service is never started - the failure mode is six services missing from docker compose ps, which looks nothing like the database problem it actually is. Crash-until-the-dependency-is-ready is the same contract Kubernetes runs on and behaves identically on every runtime.

Boot order between services is not something you can configure your way out of: in production the scheduler starts pods in whatever order it likes, so a service that cannot boot without its peers is broken there too. The previous five-deep chain (trip-service -> price-estimation-service -> surge-pricing-service -> driver-discovery-service -> redis) serialised startup and, worse, reported the blocked services as the failure instead of the broken one.

2. Containers are probed for liveness, not for the health of the world. Each image's HEALTHCHECK polls /actuator/health/liveness, which answers one question: should this process be restarted? That is the only remedy a container runtime has, and restarting a service never fixes a sick Postgres or a late Eureka.

Endpoint Includes Used by
/actuator/health/liveness livenessState container HEALTHCHECK - a failure means restart
/actuator/health/readiness readinessState + this service's own datastore traffic gating. The db/redis member is declared per module, because Actuator refuses to start when a group names a contributor the service does not have
/actuator/health everything, including Eureka and downstream indicators humans and /health/aggregate - informational

A service whose LIVE and READY are UP while its composite HEALTH is DOWN is fine; something it talks to is not. ./scripts/smoke.sh prints all three side by side for exactly this reason.

3. A missing dependency is a runtime error, not a startup error. Configured once in common/src/main/resources/uberlite-defaults.yml: missing-topics-fatal: false so a consumer outlives an absent topic, producer retries so a broker restart is not surfaced to the caller, LoadBalancer retry so a stale Eureka entry is retried against the next instance, and graceful shutdown (paired with stop_grace_period: 30s) so a rolling restart drains in-flight requests and deregisters instead of being SIGKILLed mid-request.

The visible consequence: start price-estimation-service on its own and it boots and serves /actuator/health; call it and you get a fast No instances available for route-service instead of a hang. That is the same failure mode its unit tests already cover with StubServer.

Debugging a stack that did not come up

./scripts/smoke.sh                                   # which service, and which of the three probes
docker compose ps                                    # Exited? restarting? unhealthy?
docker compose logs --tail=100 <service>             # the stack trace
curl -s localhost:<port>/actuator/health | jq        # which component is DOWN
docker inspect --format '{{json .State.Health}}' <container> | jq   # the probe's own output

Service Inventory

Core Marketplace Services (paper Sec. 4)

Service Port Purpose State
Trip Service 8083 Trip state machine, orchestrator ✅ Complete
Price Estimation Service 8085 Quote trip price ✅ Implemented
Matching Service 8089 Assign driver to trip (greedy) ✅ Implemented
Route Service 8087 Calculate distance (Haversine) ✅ Implemented
Time Estimation Service 8088 Estimate ETA (static heat map) ✅ Implemented
Surge Pricing Service 8084 Compute surge multiplier ✅ Implemented
Driver Discovery Service 8086 Query nearby drivers (Redis geo) ✅ Implemented
Tax & Tolls Service 8090 Look up tax/toll rates ✅ Implemented
Discounts & Promotions Service 8091 Apply promo codes ✅ Implemented

Background Analytics Services (paper Sec. 5)

All three are rule-based/logging-only in the MVP — no ML — and each owns its own Postgres instance ("database per service", ARCHITECTURE.md §5).

Service Port Purpose State
Forecasting Service 8092 Counts REQUESTED demand per H3 cell/hour, serves a rolling average ✅ Implemented
Matching Analytics Service 8093 Kafka consumer, persists every propose/accept/decline to match_log ✅ Implemented
Discounts Analytics Service 8094 Nightly @Scheduled batch, flags low-ride riders into promo_candidates ✅ Implemented

Forecasting Service — consumes trip-events, increments demand_counts(h3_cell, hour_of_day, day_bucket, count) on each REQUESTED event, and serves:

GET /forecast/{h3Cell}?hourOfDay=18   →  {"h3Cell":"...","hourOfDay":18,"predictedDemand":4.5}

The average is taken over the last 7 complete day buckets for that cell and hour — a bucket only counts once its hour has fully elapsed, so asking at 14:00 about the 20:00 rush is not dragged down by an evening that has not happened yet. Days with no rows count as zero demand, not as missing data. Tunable via forecasting.window-days and forecasting.zone.

This endpoint is the documented plug-in point for a future surge-pricing-service v2: Surge Pricing is currently reactive (pending_requests / active_drivers from live counters only) and would use the forecast to pre-position its clamp bounds. Not wired up in this issue — see DemandForecaster.

Matching Analytics Service — filters trip-events for DRIVER_PROPOSED, DRIVER_ACCEPTED and DRIVER_DECLINED, writing one match_log row each. This is the only place a trip's full matching history survives: Trip Service overwrites driver_id on every retry, so its own row cannot show that two drivers declined before a third accepted. Debug read: GET /match-log/{tripId}.

Discounts Analytics Service — nightly at 02:00 it calls Trip Service's GET /trips/rider-trip-counts and flags riders with fewer than discounts-analytics.trip-threshold (default 3) completed trips into promo_candidates. Riders who cross the threshold are swept out on the next run, so the promotion actually ends. The table is populated only — Discounts & Promotions' rule evaluator still prices from the live riderTripCount and reading this table is a follow-up issue. Debug: GET /promo-candidates, POST /promo-candidates/refresh (runs the batch now instead of waiting for the cron; it is idempotent).

Unlike the other two, this service has no Kafka consumer — it is a batch that pulls an aggregate, so an idle consumer group would be a dependency with nothing to do.

Infrastructure Services

Service Port Purpose
Discovery Server (Eureka) 8761 Service registry
API Gateway 8080 Single entry point + /health/aggregate
Zipkin 9411 Trace collector and UI
Kafka 9092 (in-network) / 29092 (host) trip-events topic; single-node KRaft, no ZooKeeper
Redis 6379 Driver locations, surge counters
Postgres 5433–5438 One instance per stateful service
Common — Shared DTOs, H3 utilities, Kafka event schemas, uberlite-defaults.yml

Kafka advertises two listeners: containers reach it as kafka:9092, the host as localhost:29092. A single listener cannot serve both — one side always gets an address it cannot route to.

Data Flow Example: Request a Trip

1. Rider App → API Gateway (HTTP POST /trips)
2. API Gateway → Trip Service (create trip, state=REQUESTED)
3. Trip Service → Price Estimation Service (fetch quote)
   3a. Price Estimation → Route Service (distance)
   3b. Price Estimation → Time Estimation Service (ETA)
   3c. Price Estimation → Surge Pricing Service (multiplier)
   3d. Price Estimation → Tax & Tolls Service (rates)
   3e. Price Estimation → Discounts Service (promo)
4. Trip Service publishes Kafka event (state → PRICED)
5. Trip Service → Matching Service (POST /matches → best driver, or 404 if none)
   5a. Matching → Driver Discovery Service (nearby drivers)
   5b. Matching → Route Service (per candidate, pickup ETA)
6. Trip Service publishes Kafka event (state → DRIVER_PROPOSED)
7. Matching Analytics Service (Kafka consumer) logs the match
8. Discounts Analytics Service flags low-ride riders on its own nightly schedule (it polls
   Trip Service's `/trips/rider-trip-counts`; it is not a Kafka consumer)
9. Driver App → API Gateway (accept/decline driver proposal)
10. Trip Service publishes Kafka event (state → DRIVER_ACCEPTED or DRIVER_DECLINED)
... (continue through RIDER_PICKED_UP, COMPLETED, PAID)

Trip State Machine

REQUESTED
  ↓
PRICED (Price Estimation Service)
  ↓
ACCEPTED_BY_RIDER or CANCELLED_BY_RIDER
  ↓
DRIVER_PROPOSED (Matching Service assigns candidate)
  ↓
DRIVER_ACCEPTED or DRIVER_DECLINED (retry up to 3 times)
  ↓
EN_ROUTE_TO_PICKUP
  ↓
RIDER_PICKED_UP
  ↓
COMPLETED
  ↓
PAID

Each transition publishes a Kafka event on topic trip-events for async subscribers (Matching Analytics, Discounts Analytics).

Package Structure

Each service follows the same layout. A package only appears when the service needs it — no empty placeholders — but when it appears, it has this name and no other:

<service>/
  src/main/java/com/uberlite/<pkg>/
    ├── api/                          # REST controllers
    │   ├── *Controller.java
    │   ├── ApiExceptionHandler.java  # @RestControllerAdvice, where the service has one
    │   └── dto/                      # request/response shapes used by this service only
    ├── client/                       # OpenFeign clients (for calling other services)
    │   └── *Client.java
    ├── config/                       # @ConfigurationProperties + infrastructure @Configuration
    │   └── *Properties.java, *Config.java
    ├── domain/                       # domain logic, business rules, domain exceptions
    │   └── *Service.java
    ├── messaging/                    # Kafka consumers/producers (if applicable)
    │   └── *Consumer.java
    ├── repository/                   # Spring Data repositories
    │   ├── *Repository.java
    │   └── entity/                   # @Entity classes
    │       └── *Entity.java
    └── <Name>ServiceApplication.java # Spring Boot entry point
  src/main/resources/
    ├── application.yml               # service config
    └── db/migration/                 # Flyway scripts, for modules owning a Postgres schema
  src/test/java/com/uberlite/<pkg>/
    ├── <Name>IntegrationTest.java    # whole-app tests sit in the root package
    └── <layer>/                      # unit tests mirror the package of the class under test
  Dockerfile                          # multi-stage Maven build
  README.md                           # what this module does and how to run it alone
  pom.xml                             # service-specific dependencies

Two rules are load-bearing rather than cosmetic:

  • domain/ never service/. The layer is the domain layer; the classes inside it may still be called *Service.
  • Cross-service DTOs live in common, and only there. A type produced by one service and consumed by another (RouteEstimateDto, TimeEstimateDto, PriceQuoteDto, …) is the same class on both sides, so the producer cannot change a field name without breaking the consumer's compile. api/dto/ is only for shapes nobody else consumes. A service that hand-rolls a Map as its response body defeats this entirely — the caller still binds to the shared DTO and simply gets nulls at runtime.

@EnableDiscoveryClient is deliberately absent everywhere: with spring-cloud-starter-netflix-eureka-client on the classpath registration is automatic, so the annotation was noise that only some modules carried.

Configuration

Shared defaults

Actuator exposure, tracing, and the Eureka client/instance policy are defined once, in common/src/main/resources/uberlite-defaults.yml, and pulled in by every module:

spring:
  config:
    import: "optional:classpath:uberlite-defaults.yml"

An imported document has lower precedence than the file importing it, so a service can still override anything locally. Don't copy management/tracing/eureka blocks back into a service — the whole point is that "consistent across all 14 services" stays true without anyone maintaining it.

Per-service application.yml

Only what is genuinely service-specific lives in the module:

server:
  port: 8XXX                          # unique per service, see the port table above

spring:
  config:
    import: "optional:classpath:uberlite-defaults.yml"
  application:
    name: <service-name>              # MUST match the @FeignClient(name = ...) used by callers
  datasource:
    url: jdbc:postgresql://localhost:5433/<db>   # localhost default keeps the module standalone
    username: uberlite
    password: changeme
  kafka:
    bootstrap-servers: localhost:9092

spring.application.name is load-bearing, not cosmetic: it is the Eureka service id that @FeignClient(name = ...) and the gateway's lb:// URIs resolve against. A service without one registers as UNKNOWN and every caller fails with "No instances available".

Docker profile

Each service's second YAML document overrides only the hostnames that differ inside Compose:

---
spring:
  config:
    activate:
      on-profile: docker
  datasource:
    url: jdbc:postgresql://trip-service-postgres:5432/tripdb
  kafka:
    bootstrap-servers: kafka:9092

Eureka and Zipkin addresses are not repeated per service — they come from the EUREKA_URL and ZIPKIN_ENDPOINT environment variables that docker-compose.yml sets on every container.

Run with docker compose up, or set SPRING_PROFILES_ACTIVE=docker manually.

Testing

Unit tests

./mvnw -pl <service> test

Integration tests (requires Docker running)

docker compose up -d kafka redis discovery-server trip-service-postgres
./mvnw -pl <service> -Dgroups=integration test

Full build + tests

./mvnw clean install

End-to-end smoke test

scripts/demo.sh is the executable version of the manual smoke test — it exercises every service against a running stack and exits non-zero on the first unexpected status code, so it can be run in CI as-is. See Getting Started.

Stubbing downstream services

Services that call other services over Feign (Price Estimation, Matching) are tested against StubServer, a small record-and-replay HTTP server shipped in common's test-jar:

<dependency>
    <groupId>com.uberlite</groupId>
    <artifactId>common</artifactId>
    <type>test-jar</type>
    <scope>test</scope>
</dependency>

It serves real HTTP on a random port, so the real Feign clients, JSON codecs and URLs are exercised — which is what catches a @FeignClient whose path disagrees with the downstream route.

WireMock is deliberately not used. It cannot run on this classpath: wiremock-jre8 embeds Jetty 9.4, which needs the pre-Jakarta javax.servlet API that Spring Boot 4 has dropped, and once that is patched it fails on org.eclipse.jetty.util.log.Log, a class deleted in Jetty 10 (Boot 4 manages Jetty 12). The full rationale is documented on StubServer itself. Don't re-add the dependency.

Build & Deployment

Build a single service

mvn -pl <service-name> clean package

Build all services

mvn clean install

Docker images

All 14 Dockerfiles are generated from one template in scripts/generate-dockerfiles.py and differ only in the module name and port. Edit the template and regenerate; do not hand-edit a Dockerfile:

python3 scripts/generate-dockerfiles.py          # rewrite all 14
python3 scripts/generate-dockerfiles.py --check  # CI: fail if one is stale

The port is read from the module's own application.yml, so EXPOSE and the HEALTHCHECK cannot drift from server.port.

  1. Build stage — maven:3.9-eclipse-temurin-25, compiles with -pl <module> -am. The whole repo is the context because the root pom's reactor needs every module present even for a single-module build. A BuildKit cache mount on /root/.m2 is shared across all 14 image builds, so the dependency tree is downloaded once and never lands in a layer.
  2. Layer extraction — java -Djarmode=tools … extract --layers splits the fat jar into dependencies / loader / snapshot-dependencies / application. Dependencies change rarely and application code changes every commit, so a code-only rebuild pushes a few hundred KB instead of the whole ~60 MB jar.
  3. Runtime stage — eclipse-temurin:25-jre plus curl for the probe. Runs as numeric 10001:10001 (no runAsNonRoot surprises on Kubernetes), sets JDK_JAVA_OPTIONS rather than wrapping the JVM in sh -c — so the JVM is PID 1 and receives SIGTERM directly, which is what makes graceful shutdown actually run — and declares a HEALTHCHECK against /actuator/health/liveness.

The Java version is set once in the root pom.xml (<java.version>25</java.version>) and must match the base image tags. Keep the three in sync when upgrading.

Build and run a single service image, with no other UberLite image involved:

docker build -t uberlite/route-service:latest -f route-service/Dockerfile .
docker run --rm -p 8087:8087 uberlite/route-service:latest

Key Design Decisions

  • REST + OpenFeign for synchronous inter-service calls (e.g., Price Estimation calling Route Service)
  • Kafka for asynchronous events (trip state transitions propagate to analytics without tight coupling)
  • Database per service (each has its own Postgres schema) to enforce service boundaries
  • Redis for real-time, low-latency data (driver locations, surge multipliers) that tolerates staleness
  • Eureka for dynamic service discovery so services don't hardcode ports/addresses
  • No authentication/authz in MVP (assume trusted clients; see ARCHITECTURE.md §9)
  • Rule-based algorithms (no ML) for forecasting, pricing, matching — these are plug-in points for real models later

Observability

Actuator

Every service exposes the same endpoints, because they are configured once rather than per module:

GET /actuator/health             # composite: db, redis, kafka, Eureka registration
GET /actuator/health/liveness    # "should this process be restarted?" - the container HEALTHCHECK
GET /actuator/health/readiness   # "should traffic be routed here?" - readinessState + own datastore
GET /actuator/info               # service name, JVM, OS
GET /actuator/metrics            # Micrometer metrics
GET /actuator/prometheus         # the same, in Prometheus scrape format

The split matters: the container probes liveness, so a dependency blip can never get a healthy service restarted or make Compose blame the wrong container. See Independent deployability.

Aggregate view

GET http://localhost:8080/health/aggregate

The API gateway fans out to every instance Eureka knows about and returns one document. It answers 200 only when everything is UP, 503 otherwise, so curl -f is a sufficient CI gate. Instances are reported individually, so a partial outage ("3 of 4 up") is visible rather than averaged away.

It is deliberately not folded into the gateway's own /actuator/health — the gateway's health must describe the gateway, or Compose would restart a perfectly healthy gateway because an unrelated analytics service was still booting.

Distributed tracing

Micrometer Tracing with the Brave bridge, reporting to the Zipkin container at http://localhost:9411. Trace context propagates over both HTTP hops (feign-micrometer on every service that has a Feign client) and Kafka, so a single trip request appears as one trace across every service it touches.

Sampling is set to 100% via TRACING_SAMPLE_RATE, which is a demo setting — turn it down before this ever sees real traffic.

scripts/demo.sh generates its own B3 trace id and sends it as a b3: header, then prints a direct link to the resulting trace, so verifying the fan-out never depends on guessing which trace was yours.

Troubleshooting

A service did not come up and you cannot tell which?

  • ./scripts/smoke.sh — one line per service with LIVE / READY / HEALTH. Services no longer depend on each other, so the red line is the broken service, not a victim of a boot-order chain.
  • docker compose ps, then docker compose logs --tail=100 <service> for the stack trace.
  • curl -s localhost:<port>/actuator/health | jq names the component that is DOWN (show-details: always is set for all services).
  • LIVE/READY UP but HEALTH DOWN is not a fault in that service — a dependency is missing.
  • On a cold cache the first build is slow; start_period is 60s per container before a failed probe counts against it.

A service isn't in the Eureka dashboard?

  • Check it has spring.application.name set. Without it the service registers as UNKNOWN and no @FeignClient(name = ...) or lb:// route can resolve it.
  • Check EUREKA_URL reached the container: docker compose exec <service> env | grep EUREKA.

Feign calls fail with "No instances available for X"?

  • X must exactly equal the target's spring.application.name. This is the single most common break: the id in @FeignClient(name = "…"), the gateway's lb://…, and the target's spring.application.name are three copies of one string.
  • Confirm at http://localhost:8761 that X is registered and UP.

Gateway returns 404 for a path that works against the service directly?

  • The route table is spring.cloud.gateway.server.webflux.routes in api-gateway/application.yml. Under the older spring.cloud.gateway.routes prefix the routes silently parse as nothing and the gateway starts with an empty table. ApiGatewayRoutesTest guards this.

scripts/demo.sh fails at "waiting for the stack"?

  • Read the aggregate report it dumps on failure: it names the service that is down.
  • Raise the budget with READY_TIMEOUT_SECONDS=600 on a slow machine.

Kafka producer/consumer failing?

  • From inside Compose the broker is kafka:9092; from the host it is localhost:29092. Using the wrong one gives a connection that handshakes and then times out.
  • Topic names come from common — never inline the string.

Database connection refused?

  • Default (no profile) is localhost:<published-port>; the docker profile uses the Compose hostname on 5432. The published ports are listed in the infrastructure table above.
  • Verify credentials match POSTGRES_USER/POSTGRES_PASSWORD in docker-compose.yml.

Stale schema or weird data after a rebuild?

  • docker compose down -v drops the Postgres volumes so Flyway re-runs from scratch.

Port conflicts?

  • Services occupy 8080, 8083–8094, 8761, 9411, 6379 and 5433–5438. Override a locally run service with -Dserver.port=….

Scripts

Script Purpose
scripts/demo.sh Full happy-path trip lifecycle against a running stack. Prints every request/response, exits non-zero on the first unexpected status code.
scripts/smoke.sh One line per service: liveness, readiness and composite health, from the host. Exits with the number of services that are not ready.
scripts/check-config-consistency.py Cross-checks server.port and spring.application.name against each Dockerfile, docker-compose.yml and the gateway route table.
scripts/generate-dockerfiles.py Regenerates all 14 Dockerfiles from one template. --check fails if any is stale, for CI.

server.port and spring.application.name are each duplicated in four places, and drift between them is silent — a service with the wrong name registers as UNKNOWN and callers only fail at runtime. No Java test can see across all four files, so the consistency check runs in CI.

Contributing

  • Always update tests when modifying domain logic
  • Follow the package structure above (api, api/dto, client, config, domain, messaging, repository, repository/entity) — see Package Structure
  • Use DTOs from common for cross-service communication, and return them from the producing controller too, not just from the consuming Feign client
  • Publish domain events to Kafka for async subscribers; topic names come from common
  • Every module carries its own README.md
  • Ensure mvn clean install, python3 scripts/check-config-consistency.py and python3 scripts/generate-dockerfiles.py --check all pass before pushing
  • Never add depends_on from one application service to another, and never point a container HEALTHCHECK at the composite /actuator/health — see Independent deployability

References

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages