Skip to content

Convert monolith to microservices with Spring Cloud Gateway and Eureka#265

Open
devin-ai-integration[bot] wants to merge 3 commits intoworkshop-lionel-dotnet-to-javafrom
workshop-lionel-microservices
Open

Convert monolith to microservices with Spring Cloud Gateway and Eureka#265
devin-ai-integration[bot] wants to merge 3 commits intoworkshop-lionel-dotnet-to-javafrom
workshop-lionel-microservices

Conversation

@devin-ai-integration
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot commented Apr 9, 2026

Summary

Decomposes the monolithic server/ Spring Boot application into 6 independent microservices with Spring Cloud infrastructure:

  • Eureka Server (8761) — service discovery
  • API Gateway (8080) — Spring Cloud Gateway routing /api/* to downstream services
  • Product Service (8081), Customer Service (8082), Order Service (8083), Inventory Service (8084) — domain services, each with its own H2 database

Inter-service communication (Order → Customer/Product, Inventory → Product) uses load-balanced WebClient via Eureka. Angular frontend proxies to the gateway via proxy.conf.json during development (ng serve on port 4200).

The old server/ directory and all its contents are removed. A multi-module Maven parent POM coordinates the build.

Updates since last revision

  • Fixed Angular angular.json: added required buildTarget property to the serve architect so ng serve works correctly.
  • Fixed OrderService.createOrder NPE: added null check on product.get("price") before type-branching, so a missing price field throws a clear error instead of a NullPointerException.
  • Fixed blank columns across all Angular pages:
    • Orders page: Added customerName column to CustomerOrder entity, populated from the customer-service response during order creation. Template now reads o.customerName instead of o.customer?.name.
    • Inventory page: Added productName column to InventoryItem entity, populated in the DataSeeder. Template now reads i.productName instead of i.product?.name.
    • Products page stock column: ProductListComponent now uses forkJoin to fetch both /api/products and /api/inventory in parallel, merging stock quantities by productId. Template reads p.stock instead of p.inventory?.quantityOnHand.

Review & Testing Checklist for Human

  • Order creation no longer checks or deducts inventory. The monolith's OrderService.createOrder() verified stock levels and decremented quantityOnHand atomically. The new order-service only validates customer/product existence via REST calls — it does not call inventory-service to check or deduct stock. This is a functional regression.
  • All existing tests were deleted with no replacements. The monolith had 3 integration tests in OrderServiceTest.java (empty orders, inventory deduction on order creation, insufficient stock check). Zero test classes exist in any of the new microservices. Determine if this is acceptable or if tests should be added before merge.
  • Inter-service clients silently swallow all exceptions (catch (Exception e) { return null; }). A downstream service being unavailable surfaces as a generic "Customer not found" / "Product not found" error, not a meaningful service-unavailability message. No circuit breaker or retry logic is present.
  • Products page fails entirely if inventory-service is down. The ProductListComponent uses forkJoin which requires both the products and inventory HTTP calls to succeed. If the inventory-service is unavailable, the products table won't render at all instead of gracefully showing "N/A" for stock.
  • Denormalized names may become stale. CustomerOrder.customerName is stored at order-creation time (acceptable — captures point-in-time name). InventoryItem.productName is hardcoded in the DataSeeder to match product-service seed data; if product names change in product-service, inventory will display stale names until reseeded.
  • Inventory seeder hardcodes productId values 1–5 assuming they match auto-generated IDs in the separate product-service H2 database. Since these are independent databases, IDs could diverge after restarts or data changes, causing inventory records to reference nonexistent products.
  • E2E verification: Start all 6 services in order (Eureka → Gateway → domain services), then ng serve the Angular app. Verify: (1) Eureka dashboard at :8761 shows all services registered, (2) Products page loads with stock numbers, (3) Customers page loads, (4) Inventory page shows product names, (5) Order creation via Swagger succeeds with correct total and customer name, (6) Orders page displays customer names on created orders.

Notes

  • The environment.ts sets apiUrl: 'http://localhost:8080' but no Angular component actually references environment.apiUrl — all HTTP calls use relative paths (/api/products). The proxy config is what actually routes requests during dev.
  • Gateway CORS config uses wildcard origin (*) with credentials enabled — review for security implications if this moves beyond local dev.
  • H2 databases use file-based storage with relative paths (jdbc:h2:file:./data/productdb), so data directories are created relative to wherever each service's mvn spring-boot:run is executed.
  • No Resilience4j or circuit breaker patterns were added despite the "cloud ready" goal — this may be worth discussing.

Link to Devin session: https://partner-workshops.devinenterprise.com/sessions/140ec763ab474ba295e64f13c216d195
Requested by: @lionel-john_infosys


Open with Devin

- Decompose monolithic server into 4 domain microservices:
  Product Service (8081), Customer Service (8082),
  Order Service (8083), Inventory Service (8084)
- Add Eureka Server (8761) for service discovery
- Add Spring Cloud Gateway (8080) for API routing
- Inter-service communication via load-balanced WebClient
- Each service has its own H2 database
- Spring Boot Actuator health endpoints on all services
- CORS configuration on API Gateway
- Angular proxy config for development (port 4200 -> Gateway 8080)
- Updated README with microservices architecture docs
- Parent POM for multi-module Maven build
@devin-ai-integration
Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

devin-ai-integration[bot]

This comment was marked as resolved.

…ar UI

- Add null check for product price before type branching in OrderService
- Add customerName column to CustomerOrder, populated during order creation
- Add productName column to InventoryItem, populated during data seeding
- Update Angular order-list to use o.customerName instead of o.customer?.name
- Update Angular inventory-list to use i.productName instead of i.product?.name
- Update Angular product-list to fetch inventory data and merge stock by productId
Copy link
Copy Markdown
Contributor Author

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 new potential issues.

View 9 additional findings in Devin Review.

Open in Devin Review

Comment on lines +31 to +49
- id: product-service-swagger
uri: lb://product-service
predicates:
- Path=/product-service/v3/api-docs/**

- id: customer-service-swagger
uri: lb://customer-service
predicates:
- Path=/customer-service/v3/api-docs/**

- id: order-service-swagger
uri: lb://order-service
predicates:
- Path=/order-service/v3/api-docs/**

- id: inventory-service-swagger
uri: lb://inventory-service
predicates:
- Path=/inventory-service/v3/api-docs/**
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Gateway swagger routes forward full path without stripping service-name prefix, causing 404s

The explicit swagger gateway routes (e.g., product-service-swagger with predicate Path=/product-service/v3/api-docs/**) forward the full request path including the service-name prefix to the downstream service. The product-service's API docs are at /v3/api-docs (product-service/src/main/resources/application.properties:23), not /product-service/v3/api-docs. Without a StripPrefix=1 filter, the downstream service receives the unstripped path and returns 404. Worse, since discovery.locator.enabled: true is configured (line 8), the discovery locator would auto-generate working routes that correctly strip the prefix — but the explicit swagger routes take precedence over discovery-locator routes, effectively breaking what would otherwise work automatically. All four swagger routes (lines 31-49) are affected.

Suggested change
- id: product-service-swagger
uri: lb://product-service
predicates:
- Path=/product-service/v3/api-docs/**
- id: customer-service-swagger
uri: lb://customer-service
predicates:
- Path=/customer-service/v3/api-docs/**
- id: order-service-swagger
uri: lb://order-service
predicates:
- Path=/order-service/v3/api-docs/**
- id: inventory-service-swagger
uri: lb://inventory-service
predicates:
- Path=/inventory-service/v3/api-docs/**
- id: product-service-swagger
uri: lb://product-service
predicates:
- Path=/product-service/v3/api-docs/**
filters:
- StripPrefix=1
- id: customer-service-swagger
uri: lb://customer-service
predicates:
- Path=/customer-service/v3/api-docs/**
filters:
- StripPrefix=1
- id: order-service-swagger
uri: lb://order-service
predicates:
- Path=/order-service/v3/api-docs/**
filters:
- StripPrefix=1
- id: inventory-service-swagger
uri: lb://inventory-service
predicates:
- Path=/inventory-service/v3/api-docs/**
filters:
- StripPrefix=1
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +26 to +32
forkJoin([
this.http.get<any[]>('/api/products'),
this.http.get<any[]>('/api/inventory')
]).subscribe(([products, inventory]) => {
const stockMap = new Map(inventory.map(i => [i.productId, i.quantityOnHand]));
this.products = products.map(p => ({ ...p, stock: stockMap.get(p.id) }));
});
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 forkJoin in ProductListComponent causes complete page failure when inventory-service is down

The ProductListComponent uses forkJoin to fetch products AND inventory in parallel. forkJoin requires ALL observables to complete successfully — if the inventory-service is unreachable or returns an error, the entire forkJoin observable errors out, this.products stays empty, and the products page shows nothing. In a microservices architecture, services can independently go down, so a failure in the inventory-service should not prevent displaying the product catalog. The old monolith had inventory embedded in the product response so this wasn't an issue.

Prompt for agents
The ProductListComponent uses forkJoin to fetch products and inventory simultaneously. forkJoin fails entirely if any inner observable errors, which means if the inventory-service is down the products page shows nothing.

The fix should make the inventory fetch resilient so that products are still displayed even if inventory data is unavailable. Options include:

1. Use catchError on the inventory observable to return an empty array on failure, so forkJoin still completes: this.http.get('/api/inventory').pipe(catchError(() => of([])))

2. Fetch products first and then optionally enhance with inventory data using a separate subscription.

3. Use combineLatest with startWith or a similar pattern that doesn't block on both sources.

The key file is client-app/src/app/modules/products/product-list.component.ts, specifically the ngOnInit method around lines 25-33.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants