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
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
From a clean clone to a completed trip in three commands.
| 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.
git clone <repo-url> UberLite
cd UberLite
./mvnw clean installThe 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.
docker compose --profile all up --buildFirst 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 / HEALTHThe 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 servicesAny 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.
In a second terminal:
./scripts/demo.shIt 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| 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.
docker compose down # stop everything, keep the databases
docker compose down -v # also drop the Postgres volumes for a truly clean slateEvery 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:runWithout 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.
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.
./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 | 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 |
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.
| 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.
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)
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).
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/neverservice/. 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 aMapas 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.
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.
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:9092spring.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".
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:9092Eureka 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.
./mvnw -pl <service> testdocker compose up -d kafka redis discovery-server trip-service-postgres
./mvnw -pl <service> -Dgroups=integration test./mvnw clean installscripts/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.
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.
mvn -pl <service-name> clean packagemvn clean installAll 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 staleThe port is read from the module's own application.yml, so EXPOSE and the HEALTHCHECK cannot
drift from server.port.
- 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/.m2is shared across all 14 image builds, so the dependency tree is downloaded once and never lands in a layer. - Layer extraction —
java -Djarmode=tools … extract --layerssplits 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. - Runtime stage —
eclipse-temurin:25-jrepluscurlfor the probe. Runs as numeric10001:10001(norunAsNonRootsurprises on Kubernetes), setsJDK_JAVA_OPTIONSrather than wrapping the JVM insh -c— so the JVM is PID 1 and receivesSIGTERMdirectly, which is what makes graceful shutdown actually run — and declares aHEALTHCHECKagainst/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- 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
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.
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.
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.
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, thendocker compose logs --tail=100 <service>for the stack trace.curl -s localhost:<port>/actuator/health | jqnames the component that isDOWN(show-details: alwaysis set for all services).- LIVE/READY
UPbut HEALTHDOWNis not a fault in that service — a dependency is missing. - On a cold cache the first build is slow;
start_periodis 60s per container before a failed probe counts against it.
A service isn't in the Eureka dashboard?
- Check it has
spring.application.nameset. Without it the service registers asUNKNOWNand no@FeignClient(name = ...)orlb://route can resolve it. - Check
EUREKA_URLreached the container:docker compose exec <service> env | grep EUREKA.
Feign calls fail with "No instances available for X"?
Xmust exactly equal the target'sspring.application.name. This is the single most common break: the id in@FeignClient(name = "…"), the gateway'slb://…, and the target'sspring.application.nameare three copies of one string.- Confirm at http://localhost:8761 that
Xis registered andUP.
Gateway returns 404 for a path that works against the service directly?
- The route table is
spring.cloud.gateway.server.webflux.routesinapi-gateway/application.yml. Under the olderspring.cloud.gateway.routesprefix the routes silently parse as nothing and the gateway starts with an empty table.ApiGatewayRoutesTestguards 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=600on a slow machine.
Kafka producer/consumer failing?
- From inside Compose the broker is
kafka:9092; from the host it islocalhost: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>; thedockerprofile uses the Compose hostname on 5432. The published ports are listed in the infrastructure table above. - Verify credentials match
POSTGRES_USER/POSTGRES_PASSWORDindocker-compose.yml.
Stale schema or weird data after a rebuild?
docker compose down -vdrops 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=….
| 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.
- 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
commonfor 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.pyandpython3 scripts/generate-dockerfiles.py --checkall pass before pushing - Never add
depends_onfrom one application service to another, and never point a containerHEALTHCHECKat the composite/actuator/health— see Independent deployability
- Paper: Designing UberLite: a Ride Aggregator Service (Prasaad & Vikström, UW CSE552, Fall 2019)
- Architecture spec: See
ARCHITECTURE.md - Spring Cloud docs: https://spring.io/cloud
- Kafka Java client: https://kafka.apache.org/documentation/#api
- Spring Data JPA: https://spring.io/projects/spring-data-jpa