Route Checkout to the New Pricing Service
Pricing has been extracted from the monolith into its own service, and this routes checkout to it. It adds the Deployment, Service, and VirtualService for pricing-v2, points the checkout pricing client at the new service, and removes the catalog price fallback, since pricing-v2 is now the source of truth. Retries are on in the mesh and in the client, so one bad pod won't fail a checkout. Verified in staging: quotes come back and checkout completes end to end.
Adeploy/pricing/pricing-v2.yaml1 of 4
Files Changed
deploy/pricing/
checkout/src/pricing/
checkout/src/routes/
deploy/pricing/pricing-v2.yaml
YAML · Click a line number to comment · Tap a line to comment@@ -0,0 +1,60 @@
Added: apiVersion: apps/v1
Added: kind: Deployment
Added: metadata:
Added: name: pricing-v2
Added: namespace: pricing
Added: labels:
Added: app: pricing-v2
Added: spec:
Added: replicas: 6
Added: selector:
Added: matchLabels:
Added: app: pricing-v2
Added: template:
Added: metadata:
Added: labels:
Added: app: pricing-v2
Added: spec:
Added: containers:
Added: - name: pricing
Added: image: registry.internal/pricing:2.0.0
Added: ports:
Added: - containerPort: 8080
Added: env:
Added: - name: WARM_RULES_ON_START
Added: value: "true"
Added: livenessProbe:
Added: httpGet:
Added: path: /healthz
Added: port: 8080
Added: initialDelaySeconds: 5
Added: ---
Added: apiVersion: v1
Added: kind: Service
Added: metadata:
Added: name: pricing-v2
Added: namespace: pricing
Added: spec:
Added: selector:
Added: app: pricing-v2
Added: ports:
Added: - port: 80
Added: targetPort: 8080
Added: ---
Added: apiVersion: networking.istio.io/v1beta1
Added: kind: VirtualService
Added: metadata:
Added: name: pricing
Added: namespace: pricing
Added: spec:
Added: hosts:
Added: - pricing.pricing.svc.cluster.local
Added: http:
Added: - route:
Added: - destination:
Added: host: pricing-v2.pricing.svc.cluster.local
Added: weight: 100
Added: retries:
Added: attempts: 3
Added: perTryTimeout: 2s
Added: retryOn: 5xx,reset,connect-failure
checkout/src/pricing/client.ts
TypeScript · Click a line number to comment · Tap a line to comment@@ -1,19 +1,29 @@
import { fetch } from "undici";
Removed: import { config } from "../config";
import { logger } from "../logger";
import type { Cart, Quote } from "../types";
Removed: const BASE_URL = config.monolithUrl;
Added: const BASE_URL = "http://pricing.pricing.svc.cluster.local";
Added: const MAX_ATTEMPTS = 3;
export class PricingUnavailableError extends Error {}
export async function quoteCart(cart: Cart): Promise<Quote> {
Removed: const res = await fetch(`${BASE_URL}/internal/pricing/quote`, {
Removed: method: "POST",
Removed: headers: { "content-type": "application/json" },
Removed: body: JSON.stringify(cart),
Removed: signal: AbortSignal.timeout(3000),
Removed: });
Removed: if (!res.ok) throw new PricingUnavailableError(`pricing returned ${res.status}`);
Removed: return (await res.json()) as Quote;
Added: let lastError: unknown;
Added: for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
Added: try {
Added: const res = await fetch(`${BASE_URL}/v2/quotes`, {
Added: method: "POST",
Added: headers: { "content-type": "application/json" },
Added: body: JSON.stringify({ items: cart.items, currency: cart.currency, region: cart.region }),
Added: signal: AbortSignal.timeout(5000),
Added: });
Added: if (res.ok) return (await res.json()) as Quote;
Added: lastError = new PricingUnavailableError(`pricing returned ${res.status}`);
Added: } catch (err) {
Added: lastError = err;
Added: }
Added: logger.warn({ attempt, err: lastError }, "pricing quote failed");
Added: await new Promise((resolve) => setTimeout(resolve, 200 * attempt));
Added: }
Added: throw lastError;
}
checkout/src/routes/checkout.ts
TypeScript · Click a line number to comment · Tap a line to comment@@ -38,15 +38,9 @@ export const router = Router();
router.post("/checkout/quote", requireSession, async (req, res) => {
const cart = await carts.load(req.session.cartId);
if (!cart) return res.status(404).json({ error: "cart_not_found" });
Removed: let quote: Quote;
Removed: try {
Removed: quote = await withRetry(() => quoteCart(cart), { attempts: 2 });
Removed: } catch (err) {
Removed: logger.error({ err, cartId: cart.id }, "pricing failed, using catalog prices");
Removed: quote = catalogQuote(cart);
Removed: }
Added: const quote = await withRetry(() => quoteCart(cart), { attempts: 3 });
await carts.saveQuote(cart.id, quote);
res.json(quote);
});
checkout/src/pricing/client.test.ts
TypeScript · Click a line number to comment · Tap a line to comment@@ -0,0 +1,19 @@
Added: import { MockAgent, setGlobalDispatcher } from "undici";
Added: import { describe, expect, it } from "vitest";
Added: import { aCart } from "../testing/fixtures";
Added: import { quoteCart } from "./client";
Added:
Added: describe("quoteCart", () => {
Added: it("returns the quote from the pricing service", async () => {
Added: const agent = new MockAgent();
Added: agent.disableNetConnect();
Added: setGlobalDispatcher(agent);
Added: agent
Added: .get("http://pricing.pricing.svc.cluster.local")
Added: .intercept({ path: "/v2/quotes", method: "POST" })
Added: .reply(200, { totalCents: 4200, currency: "USD" });
Added:
Added: const quote = await quoteCart(aCart());
Added: expect(quote.totalCents).toBe(4200);
Added: });
Added: });