E-Commerce Microservices Interview Questions | JiQuest

add

#

E-Commerce Microservices

Mini project / POC · end to end

E-Commerce Microservices Platform: a complete Java 21 + Spring Boot mini project.

Four services, database-per-service PostgreSQL, Kafka-driven order choreography (SAGA pattern), JWT role-based security, and AWS S3 file upload/download for product images — with architecture, ER, and sequence diagrams, real code, and the full project folder structure.

4Microservices
4Postgres DBs
5Kafka topics
Client / UI API Gateway User ServiceJWT + RBAC Product Servicecatalog + S3 Order Servicecheckout + SAGA Inventory + Notifystock + email Kafka brokerchoreography AWS S3 PostgreSQL x4one per service sync REST for lookups, async Kafka events for the order lifecycle

Project overview and requirements

This is a deliberately realistic, moderately scoped e-commerce backend: enough services to make "microservices communication" mean something, without ballooning into an unfinishable enterprise system. It's sized to actually be built, run locally with Docker Compose, and explained end to end in an interview.

Functional requirements

Auth & rolesRegister/login, issue JWTs, enforce CUSTOMER / SELLER / ADMIN roles per endpoint.
CatalogSellers create products with images; customers browse and search.
File handlingProduct images uploaded to and served from AWS S3, not the app server's disk.
CheckoutCustomers place orders; stock is reserved and confirmed asynchronously.

Non-functional requirements

Service autonomyEach service owns its own database; no service reaches into another's tables.
ResilienceInventory failures don't crash checkout — they produce a failed-order event instead.
Least privilegeEvery write endpoint is role-gated; JWTs are verified independently by every service.
Local reproducibilityThe whole stack (4 services, Kafka, 4 Postgres instances) runs via one docker compose up.

Technology stack and why each piece is there

Every technology below earns its place for a specific reason — nothing is included just to pad a resume.

TechnologyRole in this projectWhy this one
Java 21Runtime and language for all four services.Virtual threads make blocking JDBC/JPA code scale without a reactive rewrite; records simplify DTOs and events.
Spring Boot 3.xApplication framework, auto-configuration, embedded server per service.Fastest path to a production-shaped Spring service with Actuator, validation, and starters built in.
Spring Data JPAPersistence layer in every service.Repository abstraction over Hibernate keeps entity and query code declarative and testable.
Spring Security + JWTStateless authentication and role-based authorization.Stateless tokens fit a horizontally scaled, multi-service architecture better than server-side sessions.
PostgreSQLOne instance per service (database-per-service).Strong relational guarantees for money-and-inventory data, with mature JSON support where flexibility is needed.
Apache KafkaAsynchronous events driving the order lifecycle.Durable, replayable event log fits a choreographed SAGA far better than fire-and-forget messaging.
AWS S3Product image storage.Offloads binary storage from application servers; presigned URLs let clients upload directly.
OpenFeignSynchronous service-to-service REST calls.Declarative HTTP client keeps the Order → Product lookup call readable and typed.
Docker ComposeLocal orchestration of all services and infrastructure.One command reproduces the full multi-service environment for development and demos.

Jump to a section

High-level architecture

A client talks only to the API Gateway. The Gateway routes to the four services; each service independently validates the caller's JWT rather than trusting an upstream header, so security still holds even if a service is reachable directly inside the network. The Order Service is the only service both services talk to synchronously and that publishes/consumes Kafka events — it's the coordinator of the checkout SAGA, even though there's no central orchestrator process.

Client API GatewaySpring Cloud Gateway User Service/auth /users Product Service/products /files Order Service/orders Inventory + Notifystock, email user_dbPostgreSQL product_dbPostgreSQL order_dbPostgreSQL inventory_dbPostgreSQL AWS S3 Kafka broker5 topics
Why an API Gateway instead of clients calling each service directly? One public entry point means TLS termination, CORS, rate limiting, and request logging live in one place instead of being duplicated four times, and it means adding a fifth service later doesn't require every client to learn a new hostname.

Service breakdown

Each service is a separate Spring Boot application with its own database, its own deployable JAR, and its own Dockerfile. Boundaries follow business capability (auth, catalog, ordering, fulfillment), not technical layers.

User Service — identity, authentication, roles

Owns registration, login, password hashing (BCrypt), and JWT issuance. Roles are stored relationally (users, roles, user_roles) so a user can hold more than one role, for example a SELLER who is also an ADMIN during early testing.

  • POST /auth/register — create account (default role CUSTOMER)
  • POST /auth/login — verify credentials, return a signed JWT
  • GET /users/me — return the authenticated caller's profile
  • PATCH /users/{id}/roles — ADMIN-only role assignment
Owns user_dbIssues JWTsNo outbound calls to other services

Product Service — catalog and image storage

Owns products and categories, and mediates AWS S3 access for product images by issuing presigned URLs rather than proxying file bytes through the service itself. Read endpoints are public; write endpoints require SELLER or ADMIN.

  • GET /products, GET /products/{id} — public catalog browsing
  • POST /products — SELLER/ADMIN, creates a product record
  • POST /products/{id}/image-upload-url — returns a presigned S3 PUT URL
  • GET /products/{id}/image-url — returns a presigned S3 GET URL
Owns product_dbTalks to AWS S3Called synchronously by Order Service

Order Service — checkout and SAGA coordination

Owns the shopping cart-to-order lifecycle. On checkout it synchronously validates price and existence via the Product Service, persists an order in PENDING status, and publishes an order.placed event — then reacts to inventory events asynchronously to move the order to CONFIRMED or CANCELLED. It never calls Inventory synchronously; that coupling is deliberately async.

  • POST /orders — CUSTOMER, creates an order from the current cart
  • GET /orders/{id} — CUSTOMER (own orders) or ADMIN (any order)
  • GET /orders/{id}/status — poll or the client can subscribe via SSE
Owns order_dbSync calls Product ServicePublishes + consumes Kafka events

Inventory & Notification Service — stock and fulfillment

Owns stock levels and reservations, and is the only consumer of order.placed. It atomically checks and reserves stock in one transaction, then publishes either inventory.reserved or inventory.failed. The same service also consumes order.confirmed and order.cancelled to send the customer an email — bundled here as one service since notification has no state of its own worth splitting into a fifth deployable.

  • consumes order.placed → reserves stock, publishes reservation result
  • consumes order.confirmed / order.cancelled → sends email notification
  • GET /inventory/{productId} — internal/ADMIN stock lookup
Owns inventory_dbKafka consumer + producerNo inbound REST from other services

Database design: one schema per service

Every service owns its data exclusively — no foreign keys cross a service boundary, and no service queries another service's database directly. Cross-service relationships (an order item referencing a product) are represented by storing the referenced id as a plain value, not a database-level foreign key, because the referenced row physically lives in a different database.

user_db users(id PK, email, password_hash,  full_name, created_at) roles(id PK, name) user_roles(user_id FK, role_id FK) product_db categories(id PK, name) products(id PK, name, price,  category_id FK, s3_image_key,  seller_id, created_at) order_db orders(id PK, user_id,  status, total_amount,  created_at) order_items(id PK, order_id FK,  product_id, qty, unit_price) inventory_db inventory(product_id PK,  available_qty, reserved_qty) stock_reservations(id PK, order_id,  product_id, qty, status) no FK to product_db — user_id is just a stored value product_id and order_id are stored values, not DB-level FKs
Deliberate design choice This is eventual consistency by design: order_items.product_id can theoretically point at a product that was later deleted. That's an accepted trade-off of database-per-service — you gain service autonomy and independent deployability, and you pay for it with application-level referential integrity instead of a database-level guarantee.

Security and role-based access control

Authentication is stateless: the User Service issues a signed JWT containing the user id and roles as claims, and every service — not just the Gateway — independently verifies the signature and reads the roles claim before authorizing a request. This means a service is still protected even if someone reaches it directly on the internal network, bypassing the Gateway.

Client User ServicePOST /auth/login Signed JWTsub, roles, exp Any serviceJwtAuthFilter Role checkhasRole(...) 200 / 403 later calls carry Authorization: Bearer <jwt>

Roles and what they can do

RoleCan do
ROLE_CUSTOMERBrowse products, place orders, view own orders and profile.
ROLE_SELLEREverything CUSTOMER can, plus create/update own products and upload product images.
ROLE_ADMINManage any user's roles, view all orders, manage categories.
// SecurityConfig.java (Product Service)
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    return http
        .csrf(csrf -> csrf.disable())
        .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
        .authorizeHttpRequests(auth -> auth
            .requestMatchers(HttpMethod.GET, "/products/**").permitAll()
            .requestMatchers(HttpMethod.POST, "/products/**").hasAnyRole("SELLER", "ADMIN")
            .requestMatchers(HttpMethod.DELETE, "/products/**").hasRole("ADMIN")
            .anyRequest().authenticated())
        .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
        .build();
}
// JwtAuthFilter.java (shared pattern across all four services)
public class JwtAuthFilter extends OncePerRequestFilter {
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
            throws ServletException, IOException {
        String header = req.getHeader("Authorization");
        if (header != null && header.startsWith("Bearer ")) {
            Jws jws = jwtService.parseAndValidate(header.substring(7));
            String userId = jws.getBody().getSubject();
            List roles = jws.getBody().get("roles", List.class);
            var authorities = roles.stream().map(SimpleGrantedAuthority::new).toList();
            var authToken = new UsernamePasswordAuthenticationToken(userId, null, authorities);
            SecurityContextHolder.getContext().setAuthentication(authToken);
        }
        chain.doFilter(req, res);
    }
}
Security note Every service verifies the JWT signature itself using a shared public key (or a shared secret for HMAC) — never trust an unsigned header like X-User-Roles set by a gateway, since anything reachable directly on the network could forge it.

Service-to-service communication: sync REST + async Kafka SAGA

Two different communication styles are used deliberately, not interchangeably. Synchronous REST (via OpenFeign) is used only where the caller needs an immediate answer to proceed — validating a product exists and its price before creating an order. Everything about the order's downstream fulfillment is asynchronous, coordinated through Kafka events with no central orchestrator: this is a choreographed SAGA.

Kafka topics

TopicProducerConsumer(s)Payload
order.placedOrder ServiceInventory ServiceorderId, items[productId, qty]
inventory.reservedInventory ServiceOrder ServiceorderId, reservationId
inventory.failedInventory ServiceOrder ServiceorderId, reason
order.confirmedOrder ServiceNotification (same service as Inventory)orderId, userId
order.cancelledOrder ServiceNotification (same service as Inventory)orderId, userId, reason
Client Order Service Product Service Kafka Inventory Svc 1. POST /orders 2. GET price (sync) 3. price + stock flag 4. save order (PENDING) 5. publish order.placed 6. consume 7. reserve stock 8. inventory.reserved 9. consume 10. order → CONFIRMED client polls / SSE update
// OrderService.java -- synchronous validation, then async handoff
public Order checkout(Long userId, List cart) {
    List items = cart.stream().map(line -> {
        ProductDto product = productClient.getProduct(line.productId()); // OpenFeign, sync
        return new OrderItem(line.productId(), line.qty(), product.price());
    }).toList();

    Order order = orderRepository.save(Order.pending(userId, items));
    kafkaTemplate.send("order.placed",
        new OrderPlacedEvent(order.getId(), items));
    return order;
}
// InventoryEventListener.java -- reacts to order.placed
@KafkaListener(topics = "order.placed", groupId = "inventory-service")
public void onOrderPlaced(OrderPlacedEvent event) {
    boolean reserved = inventoryService.tryReserve(event.orderId(), event.items()); // one DB transaction
    String topic = reserved ? "inventory.reserved" : "inventory.failed";
    kafkaTemplate.send(topic, new InventoryResultEvent(event.orderId(), reserved));
}
Why choreography instead of a central orchestrator? With only two downstream steps (reserve stock, notify), a dedicated orchestrator service would add a fifth moving part for very little coordination complexity. Choreography keeps each service simpler at this scale; a real orchestrator (via a framework or a dedicated saga-coordinator service) earns its cost once the number of steps and failure-compensation paths grows.

AWS S3: product image upload and download

Product images never pass through the Product Service's own memory or disk. The service only issues short-lived, permission-scoped presigned URLs; the browser uploads and downloads directly against S3. This keeps the service stateless and avoids it becoming a bottleneck for large file transfers.

// S3StorageService.java
@Service
public class S3StorageService {
    private final S3Presigner presigner;
    private final String bucket = "jiquest-ecommerce-product-images";

    public URL generateUploadUrl(String productId, String contentType) {
        PutObjectRequest putRequest = PutObjectRequest.builder()
            .bucket(bucket)
            .key("products/" + productId + "/" + UUID.randomUUID())
            .contentType(contentType)
            .build();
        PresignedPutObjectRequest presigned = presigner.presignPutObject(b -> b
            .signatureDuration(Duration.ofMinutes(5))
            .putObjectRequest(putRequest));
        return presigned.url();
    }

    public URL generateDownloadUrl(String s3Key) {
        GetObjectRequest getRequest = GetObjectRequest.builder().bucket(bucket).key(s3Key).build();
        PresignedGetObjectRequest presigned = presigner.presignGetObject(b -> b
            .signatureDuration(Duration.ofMinutes(10))
            .getObjectRequest(getRequest));
        return presigned.url();
    }
}
  1. Client calls POST /products/{id}/image-upload-url (SELLER/ADMIN only) and gets back a presigned S3 PUT URL.
  2. Client uploads the image bytes directly to that S3 URL — the Product Service is not in this data path at all.
  3. Client calls back PATCH /products/{id} with the resulting S3 object key, which the service stores as s3_image_key.
  4. Anyone viewing the product calls GET /products/{id}/image-url, which returns a short-lived presigned GET URL for the browser to load the image from directly.
Production note In front of S3, a real deployment adds CloudFront for caching and a custom domain instead of serving presigned URLs straight from the S3 endpoint — presigned URLs are still the right mechanism for the upload side either way.

Project folder structure

A single monorepo with one Maven module per service keeps this buildable and browsable as one unit, while each service still ships as its own independent JAR and Docker image.

ecommerce-microservices-poc/
├── api-gateway/
│  ├── src/main/java/com/jiquest/gateway/GatewayApplication.java
│  ├── src/main/resources/application.yml
│  ├── Dockerfile
│  └── pom.xml
│
├── user-service/
│  ├── src/main/java/com/jiquest/user/
│  │  ├── UserServiceApplication.java
│  │  ├── config/SecurityConfig.java
│  │  ├── controller/AuthController.java
│  │  ├── controller/UserController.java
│  │  ├── service/AuthService.java
│  │  ├── service/JwtService.java
│  │  ├── repository/UserRepository.java
│  │  ├── repository/RoleRepository.java
│  │  ├── entity/User.java
│  │  ├── entity/Role.java
│  │  ├── dto/LoginRequest.java, RegisterRequest.java
│  │  └── security/JwtAuthFilter.java
│  ├── src/main/resources/application.yml
│  ├── src/test/java/com/jiquest/user/AuthControllerIT.java
│  ├── Dockerfile
│  └── pom.xml
│
├── product-service/
│  ├── src/main/java/com/jiquest/product/
│  │  ├── ProductServiceApplication.java
│  │  ├── config/SecurityConfig.java, S3Config.java
│  │  ├── controller/ProductController.java, FileController.java
│  │  ├── service/ProductService.java, S3StorageService.java
│  │  ├── repository/ProductRepository.java, CategoryRepository.java
│  │  └── entity/Product.java, Category.java
│  ├── src/main/resources/application.yml
│  ├── Dockerfile
│  └── pom.xml
│
├── order-service/
│  ├── src/main/java/com/jiquest/order/
│  │  ├── OrderServiceApplication.java
│  │  ├── client/ProductClient.java          // OpenFeign
│  │  ├── config/SecurityConfig.java, KafkaProducerConfig.java, KafkaConsumerConfig.java
│  │  ├── controller/OrderController.java
│  │  ├── service/OrderService.java
│  │  ├── event/OrderPlacedEvent.java, InventoryResultListener.java
│  │  ├── repository/OrderRepository.java
│  │  └── entity/Order.java, OrderItem.java
│  ├── src/main/resources/application.yml
│  ├── Dockerfile
│  └── pom.xml
│
├── inventory-notification-service/
│  ├── src/main/java/com/jiquest/inventory/
│  │  ├── InventoryServiceApplication.java
│  │  ├── config/KafkaConsumerConfig.java, KafkaProducerConfig.java
│  │  ├── listener/OrderPlacedListener.java, OrderStatusListener.java
│  │  ├── service/InventoryService.java, NotificationService.java
│  │  ├── repository/InventoryRepository.java, ReservationRepository.java
│  │  └── entity/Inventory.java, StockReservation.java
│  ├── src/main/resources/application.yml
│  ├── Dockerfile
│  └── pom.xml
│
├── docker-compose.yml
├── .env.example
└── README.md

Local development setup

The entire stack — four services, four Postgres instances, and Kafka — starts with one command.

# docker-compose.yml (excerpt)
services:
  postgres-user:
    image: postgres:16
    environment: { POSTGRES_DB: user_db, POSTGRES_PASSWORD: postgres }
    ports: ["5433:5432"]

  postgres-product:
    image: postgres:16
    environment: { POSTGRES_DB: product_db, POSTGRES_PASSWORD: postgres }
    ports: ["5434:5432"]

  postgres-order:
    image: postgres:16
    environment: { POSTGRES_DB: order_db, POSTGRES_PASSWORD: postgres }
    ports: ["5435:5432"]

  postgres-inventory:
    image: postgres:16
    environment: { POSTGRES_DB: inventory_db, POSTGRES_PASSWORD: postgres }
    ports: ["5436:5432"]

  kafka:
    image: apache/kafka:3.7.0
    ports: ["9092:9092"]
    environment:
      KAFKA_NODE_ID: 1
      KAFKA_PROCESS_ROLES: broker,controller
      KAFKA_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093

  user-service:
    build: ./user-service
    depends_on: [postgres-user]
    environment:
      SPRING_DATASOURCE_URL: jdbc:postgresql://postgres-user:5432/user_db
    ports: ["8081:8080"]

  # product-service, order-service, inventory-notification-service follow the same shape,
  # each pointing at its own postgres-* host and the shared kafka:9092 broker.

  api-gateway:
    build: ./api-gateway
    depends_on: [user-service, product-service, order-service, inventory-notification-service]
    ports: ["8080:8080"]

Run docker compose up --build, then hit the gateway at localhost:8080. AWS credentials for S3 are supplied via environment variables (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY) or, for fully offline local development, pointed at a LocalStack S3 container instead of real AWS.

Testing strategy

Unit tests cover service logic with mocked repositories; integration tests use Testcontainers so the test suite exercises real PostgreSQL and real Kafka instead of embedded fakes that can behave subtly differently in production.

@Testcontainers
@SpringBootTest
class InventoryReservationIT {

    @Container
    static PostgreSQLContainer postgres = new PostgreSQLContainer<>("postgres:16");

    @Container
    static KafkaContainer kafka = new KafkaContainer(DockerImageName.parse("apache/kafka:3.7.0"));

    @DynamicPropertySource
    static void props(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.kafka.bootstrap-servers", kafka::getBootstrapServers);
    }

    @Test
    void reservesStockWhenAvailableAndPublishesReservedEvent() {
        // publish an order.placed test event, then assert inventory_db was
        // decremented and an inventory.reserved event was published
    }
}
Testcontainers PostgresTestcontainers KafkaWireMock for OpenFeign in Order Service tests

Path to production on AWS

This POC is scoped for local Docker Compose, but every piece maps cleanly onto managed AWS services when it's time to deploy for real.

Local (this POC)AWS equivalent
4 services in Docker ComposeECS Fargate or EKS, one service/task-definition per microservice
4 Postgres containers4 Amazon RDS PostgreSQL instances (or one instance, 4 databases, if cost matters more than isolation)
Single-broker Kafka containerAmazon MSK (managed Kafka)
S3 with local credentialsS3 + CloudFront, IAM roles per task instead of static keys
Manual docker compose upInfrastructure as code (Terraform) provisioning all of the above

Terraform, ECS/EKS, and observability (centralized logging, distributed tracing) are natural next layers on top of this same design — worth their own dedicated follow-up build once this core service set is solid.

Key design decisions and interview talking points

These are the questions an interviewer is most likely to ask about a project like this — each answer is also the reasoning that actually shaped the design above.

Why database-per-service instead of one shared PostgreSQL database?

A shared database lets any service accidentally couple to another's internal schema, so a column rename in one team's table silently breaks another team's queries. Database-per-service forces every cross-service interaction through an explicit API or event contract, which is slower to build but keeps services independently deployable — the entire point of choosing microservices in the first place.

Why Kafka instead of direct REST calls for the inventory step of checkout?

If Order called Inventory synchronously and Inventory was slow or down, checkout itself would fail or hang, coupling the availability of one service to another's. Publishing an event lets Order return quickly with a PENDING order, and lets Inventory process at its own pace — including retrying safely from Kafka's durable log if Inventory itself restarts mid-processing.

How do you keep the inventory.placed consumer idempotent if Kafka redelivers a message?

Kafka's at-least-once delivery means the same event can arrive twice. The reservation table stores order_id as a unique constraint, so a duplicate order.placed event for an order that already has a reservation is a no-op detected at the database level, not something the consumer has to track in memory.

Why does Order Service call Product Service synchronously but never Inventory Service synchronously?

The price and existence check is needed immediately to build a valid order line, and a brief synchronous call is acceptable for that read-only lookup. Stock reservation, by contrast, can legitimately take time (it's a write, potentially contended under load), and the checkout flow should not block on it — that's exactly the shape of problem asynchronous events solve.

What happens to an order if the Order Service crashes right after publishing order.placed but before persisting the PENDING order?

The design in this POC intentionally persists the order before publishing the event, not after, precisely to avoid that gap — a "phantom" event with no corresponding order. The realistic remaining risk is the reverse: the order is saved but the publish fails; the fix for that is the transactional outbox pattern (write the event to an outbox table in the same DB transaction, then a separate relay publishes it to Kafka), which is a natural hardening step beyond this POC's scope.

Why JWT/stateless authentication instead of server-side sessions?

A session store would need to be shared and available to all four services, becoming another piece of shared infrastructure and a single point of coupling. A signed JWT carries its own proof of authenticity, so any service can verify a caller independently with just a public key, no shared session store or network call required.

Why validate the JWT independently in every service instead of only at the API Gateway?

Trusting the Gateway's validation and passing an unsigned internal header forward means any service reachable directly on the internal network — bypassing the Gateway, whether by misconfiguration or a compromised host — would accept forged identity headers. Independent verification in every service is defense in depth: the Gateway becomes a convenience, not a security boundary the whole system depends on.

Why presigned S3 URLs instead of uploading the file through the Product Service?

Routing file bytes through the service means the service's memory, threads, and bandwidth scale with upload traffic, not just request logic — a large image upload can tie up a request thread for seconds. Presigned URLs let the client talk to S3 directly; the service's only job is issuing a short-lived, scoped permission slip, which keeps it lightweight and horizontally scalable.

How would you prevent a SELLER from uploading an image to another seller's product using a presigned URL?

Authorization happens before the presigned URL is ever issued: POST /products/{id}/image-upload-url checks that the authenticated caller's user id matches the product's seller_id (or the caller is ADMIN) before generating the URL. The presigned URL itself is scoped to one specific S3 key, so even a leaked URL can't be used to overwrite a different product's image.

Why is Notification bundled into the Inventory service instead of being its own fifth microservice?

Notification has no meaningful state of its own to own — it only reacts to events and calls an email provider. Splitting it out would add a fifth deployable, a fifth thing to monitor, and a fifth set of infrastructure, for a component with no independent scaling or ownership need at this project's scale. This is a judgment call, not a rule — a real e-commerce platform with high notification volume would likely split it out.

How would you handle a partial failure where inventory reservation succeeds but the notification email fails to send?

The order's business state (CONFIRMED) should not depend on the notification succeeding — notification failures are logged and retried independently (or sent to a dead-letter topic for manual review), never allowed to roll back or block the order status update, because the customer having a confirmed order is the more important guarantee than them receiving an email promptly.

Why choreography (services reacting to each other's events) instead of a central saga orchestrator here?

Orchestration centralizes the workflow logic in one coordinator, which is easier to reason about as the number of steps grows, at the cost of a new service that becomes a critical dependency. With only one downstream reaction (inventory, which triggers notification), choreography keeps total moving parts lower; the trade-off flips once you'd need five or six coordinated steps with complex compensation logic.

What's the compensating action if inventory reservation fails after the order was already shown to the customer as "placed"?

The Order Service consumes inventory.failed and transitions the order to CANCELLED rather than leaving it stuck in PENDING, then publishes order.cancelled so the Notification listener can inform the customer. This is the compensating transaction half of the SAGA pattern — undoing forward progress with a new action, since there's no cross-service database rollback available.

How would you extend this project to demonstrate infrastructure as code without over-scoping the core build?

Add Terraform modules that provision the AWS equivalents from the production-mapping table above — VPC, RDS instances, an MSK cluster, an S3 bucket with a lifecycle policy, and ECS task definitions — as a clearly separate, optional phase on top of the working application, rather than building infrastructure and application code simultaneously and risking neither being finished.

What would you point to as the single best design decision in this project if an interviewer asks you to pick one?

The split between synchronous validation (Order → Product) and asynchronous fulfillment (Order ↔ Inventory via Kafka) — because it's the decision that most directly demonstrates understanding when to accept the complexity of eventual consistency versus when a simple blocking call is the right, simpler tool, which is the core judgment call microservices architecture actually tests.

Related guides

Update these hrefs to your published Blogger post URLs once each page is live.

No comments
Leave a Comment