Definition
- Per-service signals – liveness/readiness probes, CPU, memory, HTTP 5xx rate. These tell you each individual service is running.
- Seam failures – bad data, a flipped feature flag, or a silent logical error causes no crash in any service but breaks the composed business flow.
- Business-outcome metrics – payment-success rate, order-completion rate. Measured at the outcome boundary, not inside one service.
- Synthetic journeys – an automated client that continuously executes a real end-to-end flow against production, asserting the business outcome.
- Complementary layers – business metric says 'product is broken,' synthetic journey says 'where in the flow,' per-service dashboards and traces say 'why.'
Imagine National Bank of India's branch where every department reports 'we are open and staffed.' The teller counter is green, the vault is green, the signature-verification desk is green. Yet Rajesh cannot complete a transfer, because the form passed between departments has the wrong field filled in. Every department is 'healthy,' but the customer walked out without his transfer. A synthetic journey is like sending a secret shopper through the whole process every minute and raising an alarm the moment the end-to-end transfer fails.
Understanding the Concepts
Why can all services be green while the product is broken?
The core issue is a mismatch of scope. A readiness probe answers: 'Can this one Pod accept traffic right now?' A 5xx rate answers: 'Is this one service throwing server errors?' Both can be perfectly healthy while the business outcome fails.
Scenario: Feature flag accidentally turned off.
Payment Service:
Health check: 200 OK (green)
CPU: 15% (normal)
5xx rate: 0% (perfect)
What it does: accepts every transfer, returns HTTP 200,
writes status as "INITIATED" but the settlement flag is OFF.
No payment ever moves to "COMPLETED".
Result: every dashboard is green, but no customer's money moves.
What are the common ways this happens?
The failures that slip through live in the seams between services, not inside any single service.
package com.bank.seam;
import java.math.BigDecimal;
import java.util.logging.Logger;
public class SeamFailureExamples {
private static final Logger LOG = Logger.getLogger(SeamFailureExamples.class.getName());
// Scenario 1: Feature flag flipped off.
// Payment Service returns 200 but marks as "INITIATED" forever.
public void scenarioFeatureFlag(String paymentId) {
boolean settlementEnabled = false; // accidentally turned off
if (settlementEnabled) {
// settle(paymentId);
}
LOG.info("paymentId=" + paymentId + " status=INITIATED"); // no error logged
}
// Scenario 2: Wrong exchange rate from pricing service.
// Transfer "succeeds" but with incorrect amounts.
public void scenarioWrongData(String paymentId, BigDecimal rate) {
BigDecimal amount = new BigDecimal("50000").multiply(rate); // rate is wrong
LOG.info("paymentId=" + paymentId + " converted amount=" + amount); // looks fine
}
// Scenario 3: Schema change causes silent field drop.
// Notification service consumes events but drops them because
// a required field is now null after a schema change.
public void scenarioSchemaChange(String paymentId, String email) {
if (email == null) {
LOG.info("paymentId=" + paymentId + " skipping notification (no email)");
return; // silently does nothing, no error
}
}
}
In all three scenarios:
- Health checks: 200 OK (green)
- CPU/memory: normal
- Error rate: 0%
- Per-service dashboard: all green
- Customer experience: broken
What are business-outcome metrics?
Instead of only counting HTTP statuses, emit a metric for the thing the customer actually wanted.
package com.bank.metrics;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.stereotype.Component;
@Component
public class TransferOutcomeMetrics {
private final Counter initiated;
private final Counter completed;
private final Counter failed;
public TransferOutcomeMetrics(MeterRegistry registry) {
this.initiated = registry.counter("transfers.initiated");
this.completed = registry.counter("transfers.completed");
this.failed = registry.counter("transfers.failed");
}
public void recordInitiated() { initiated.increment(); }
public void recordCompleted() { completed.increment(); }
public void recordFailed() { failed.increment(); }
}
Prometheus metrics:
transfers_initiated_total 1523
transfers_completed_total 0 <- ALERT: completions dropped to zero
transfers_failed_total 0
Payment-success rate = completed / initiated = 0 / 1523 = 0%
Alert fires: "Transfer completion rate below 95% SLO"
All per-service dashboards are still green.
How do you define SLIs and SLOs for business metrics?
A Service Level Indicator (SLI) is the metric you measure. A Service Level Objective (SLO) is the target you alert on.
SLI: Transfer completion rate = transfers.completed / transfers.initiated
SLO: Transfer completion rate >= 99.5% over 5-minute window
Prometheus alerting rule:
alert: TransferCompletionRateLow
expr: rate(transfers_completed_total[5m]) / rate(transfers_initiated_total[5m]) < 0.995
for: 2m
labels:
severity: critical
annotations:
summary: "Transfer completion rate below 99.5% SLO"
What are synthetic journeys?
A synthetic journey is an automated client that continuously performs a real end-to-end flow against production.
package com.bank.synthetic;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import java.math.BigDecimal;
import java.util.logging.Logger;
@Component
public class SyntheticTransferProbe {
private static final Logger LOG = Logger.getLogger(SyntheticTransferProbe.class.getName());
private final RestTemplate restTemplate;
public SyntheticTransferProbe(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
@Scheduled(fixedRate = 60000) // every 60 seconds
public void runSyntheticTransfer() {
try {
// Use dedicated synthetic accounts that never affect real customers
var request = new TransferRequest(
"SYNTH-FROM-001", "SYNTH-TO-001", new BigDecimal("1.00"));
var response = restTemplate.postForEntity(
"http://api-gateway/api/v1/transfers", request, TransferResponse.class);
// Assert the transfer reaches COMPLETED within 30 seconds
String paymentId = response.getBody().getPaymentId();
boolean completed = pollForCompletion(paymentId, 30_000);
if (completed) {
LOG.info("Synthetic transfer PASSED: " + paymentId);
} else {
LOG.severe("Synthetic transfer FAILED: " + paymentId
+ " did not complete within 30s");
// Fire alert
}
} catch (Exception e) {
LOG.severe("Synthetic transfer ERROR: " + e.getMessage());
}
}
private boolean pollForCompletion(String paymentId, long timeoutMs) {
// Poll payment status until COMPLETED or timeout
return true; // simplified
}
}
Expected behavior:
Every 60 seconds: create test transfer from SYNTH-FROM-001 to SYNTH-TO-001
Assert: transfer reaches COMPLETED within 30 seconds
Settlement flag OFF -> synthetic transfer stays INITIATED -> alert fires
All per-service dashboards still green, but synthetic probe caught it.
How do the three layers complement each other?
Business metrics, synthetic journeys, and per-service probes form three complementary layers.
| Layer | Question Answered | Signal Speed |
|---|---|---|
| Business-outcome metrics | Is the customer okay? | Within minutes (needs traffic) |
| Synthetic journeys | Where in the flow does it break? | Within 1 minute (runs continuously) |
| Per-service dashboards + traces | Why is it broken? | Immediate (always available) |
What are common mistakes?
The most common mistake is assuming green health checks mean the product works.
package com.bank.antipattern;
// WRONG: Only per-service health checks. Misses composition-level failures.
public class WrongMonitoring {
// readinessProbe: /actuator/health -> 200 OK
// Conclusion: "Everything is fine."
// Reality: No transfer has completed in 2 hours.
}
// CORRECT: Business metrics + synthetic journeys + per-service probes.
public class CorrectMonitoring {
// 1. transfers.completed / transfers.initiated -> alert if < 99.5%
// 2. Synthetic probe every 60s -> alert if transfer does not complete
// 3. Per-service probes -> localize the root cause
}
What should a senior engineer know?
Tag synthetic traffic so it never pollutes real business metrics. Use dedicated synthetic accounts. Treat business metrics as SLIs with SLOs. The business metric says the product is broken, the synthetic journey says where, and the per-service dashboards and traces say why.
Visual Overview
Three layers of observability: Layer 1: Business Outcome Metrics ┌─────────────────────────────────────────┐ │ transfers.initiated: 1523 │ │ transfers.completed: 0 <- ALERT │ │ "Is the customer okay?" -> NO │ └─────────────────────────────────────────┘ Layer 2: Synthetic Journeys ┌─────────────────────────────────────────┐ │ Every 60s: create test transfer │ │ Assert: reaches COMPLETED in 30s │ │ Result: FAILED at step 3 (settlement) │ │ "Where in the flow?" -> Settlement step │ └─────────────────────────────────────────┘ Layer 3: Per-Service Dashboards + Traces ┌─────────────────────────────────────────┐ │ Payment Service: 200 OK, 0% errors │ │ Settlement Service: 200 OK, 0% errors │ │ Trace: settlement flag = OFF │ │ "Why?" -> Feature flag accidentally off │ └─────────────────────────────────────────┘
Key Takeaway
Every service can be green while the product is broken because per-service signals and business outcomes measure completely different things. A readiness probe only answers whether one Pod can accept traffic. A low 5xx rate only means the service is not throwing server errors. None of these say whether the composed end-to-end flow that Rajesh cares about actually succeeds. The failures that slip through live in the seams: a flipped feature flag, bad data passed downstream, or a silent logical error.
The fix is to measure the outcome, not just the parts. Business-outcome metrics like transfer-completion rate alert when the ratio drops, even though every per-service dashboard is green. Synthetic journeys run a real end-to-end flow every minute, catching seam failures even during quiet hours when real traffic is too sparse for statistical significance.
At National Bank of India, Pankaj deployed a change with a settlement flag accidentally turned off. All weekend every dashboard was green, but no transfer completed. After adding a payment-completion-rate SLO and a synthetic journey, the next composition-level fault was caught within a minute.
Remember one sentence: green services only prove the parts are alive, so always add business-outcome metrics and synthetic journeys that measure what the customer actually experiences.
---