Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Phase tracking:
- [Phase 1 — Production-ready reference baseline](docs/phase-1-production-readiness.md)
- [Phase 2 — Expand from GoF to enterprise patterns](docs/phase-2-enterprise-patterns.md)
- [Phase 3 — Modern Java implementation variants](docs/phase-3-modern-java-variants.md)
- [Phase 4 — Cloud-native + observability examples](docs/phase-4-cloud-native-observability.md)

---

Expand Down Expand Up @@ -100,6 +101,14 @@ Package: `src/main/java/org/example/architecture`
- **Strangler Fig migration sample** (`strangler`)
- **Anti-Corruption Layer** (`acl`)

### 9) Cloud-native + observability examples
Package: `src/main/java/org/example/cloudnative/observability`

- **Command latency tracing** (OpenTelemetry spans + latency histogram)
- **Circuit breaker metrics** (success/failure/rejection counters)
- **Saga correlation IDs** (trace attributes and correlated execution)
- **Resilience fault-injection scenarios** (Testcontainers + WireMock)

---

## Pattern index matrix
Expand Down
45 changes: 45 additions & 0 deletions deploy/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
version: "3.9"

services:
command-observability-demo:
image: ghcr.io/janaka2/designpatterns-command-demo:latest
environment:
OTEL_SERVICE_NAME: command-observability-demo
OTEL_EXPORTER_OTLP_ENDPOINT: http://otel-collector:4317
ports:
- "8088:8080"
depends_on:
- otel-collector

resilience-fault-demo:
image: ghcr.io/janaka2/designpatterns-resilience-demo:latest
environment:
UPSTREAM_BASE_URL: http://wiremock:8080
OTEL_EXPORTER_OTLP_ENDPOINT: http://otel-collector:4317
ports:
- "8090:8080"
depends_on:
- wiremock
- otel-collector

wiremock:
image: wiremock/wiremock:3.13.1
command: ["--verbose"]
ports:
- "8089:8080"

otel-collector:
image: otel/opentelemetry-collector:0.122.1
command: ["--config=/etc/otelcol/config.yaml"]
volumes:
- ./otel-collector-config.yaml:/etc/otelcol/config.yaml:ro
ports:
- "4317:4317"
- "4318:4318"
depends_on:
- jaeger

jaeger:
image: jaegertracing/all-in-one:1.64.0
ports:
- "16686:16686"
35 changes: 35 additions & 0 deletions deploy/k8s/command-observability-demo.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: command-observability-demo
spec:
replicas: 1
selector:
matchLabels:
app: command-observability-demo
template:
metadata:
labels:
app: command-observability-demo
spec:
containers:
- name: app
image: ghcr.io/janaka2/designpatterns-command-demo:latest
ports:
- containerPort: 8080
env:
- name: OTEL_SERVICE_NAME
value: command-observability-demo
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: http://otel-collector:4317
---
apiVersion: v1
kind: Service
metadata:
name: command-observability-demo
spec:
selector:
app: command-observability-demo
ports:
- port: 80
targetPort: 8080
55 changes: 55 additions & 0 deletions deploy/k8s/resilience-fault-demo.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: resilience-fault-demo
spec:
replicas: 1
selector:
matchLabels:
app: resilience-fault-demo
template:
metadata:
labels:
app: resilience-fault-demo
spec:
containers:
- name: app
image: ghcr.io/janaka2/designpatterns-resilience-demo:latest
ports:
- containerPort: 8080
env:
- name: UPSTREAM_BASE_URL
value: http://wiremock:8080
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: http://otel-collector:4317
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: wiremock
spec:
replicas: 1
selector:
matchLabels:
app: wiremock
template:
metadata:
labels:
app: wiremock
spec:
containers:
- name: wiremock
image: wiremock/wiremock:3.13.1
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: wiremock
spec:
selector:
app: wiremock
ports:
- port: 8080
targetPort: 8080
21 changes: 21 additions & 0 deletions deploy/otel-collector-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
receivers:
otlp:
protocols:
grpc:
http:

exporters:
debug:
otlp/jaeger:
endpoint: jaeger:4317
tls:
insecure: true

service:
pipelines:
traces:
receivers: [otlp]
exporters: [debug, otlp/jaeger]
metrics:
receivers: [otlp]
exporters: [debug]
46 changes: 46 additions & 0 deletions docs/phase-4-cloud-native-observability.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Phase 4 — Cloud-native + observability examples

This phase adds observability-first examples and deployment-ready scaffolding for running selected pattern demos in containerized environments.

## Observability-first pattern examples

Package: `org.example.cloudnative.observability`

### 1) Command latency trace
- `ObservedCommandExecutor` wraps command execution with an OpenTelemetry span (`command.execute`).
- Records latency metric `pattern.command.latency.ms` to make command performance visible in dashboards.

### 2) Circuit breaker metrics
- `ObservedCircuitBreaker` wraps `CircuitBreaker` and emits counter metric `pattern.circuit_breaker.executions`.
- Counter outcome labels: `success`, `failure`, `rejected` (open circuit).

### 3) Saga correlation IDs
- `CorrelatedSagaOrchestrator` accepts a `correlationId` and propagates it through:
- saga orchestration span,
- per-step action span,
- compensation spans on rollback.
- `CorrelatedSagaStep` keeps saga step contracts explicit and correlation-aware.

## Resilience test scenarios

`ResilienceFaultInjectionTest` introduces fault injection via **Testcontainers + WireMock**:

- timeout behavior (`/timeout` fixed delay)
- partial/transient failures (`/inventory` stateful scenario)
- retries via `RetryExecutor`

This verifies that resilience patterns remain understandable under stress and failure conditions.

## Deployment-ready examples

Minimal deployment assets are included under `deploy/`:

- `deploy/docker-compose.yml`
- command observability demo
- resilience fault demo
- WireMock dependency simulator
- OpenTelemetry Collector + Jaeger
- `deploy/k8s/command-observability-demo.yaml`
- `deploy/k8s/resilience-fault-demo.yaml`

These manifests are intentionally minimal so teams can copy them as a starting point for real service demos.
24 changes: 24 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
<checkstyle.version>10.12.4</checkstyle.version>
<spotbugs.plugin.version>4.8.6.6</spotbugs.plugin.version>
<jacoco.version>0.8.12</jacoco.version>
<opentelemetry.version>1.46.0</opentelemetry.version>
<testcontainers.version>1.20.6</testcontainers.version>
</properties>

<dependencies>
Expand Down Expand Up @@ -56,6 +58,28 @@
<version>${reactor.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-api</artifactId>
<version>${opentelemetry.version}</version>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-sdk</artifactId>
<version>${opentelemetry.version}</version>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-sdk-testing</artifactId>
<version>${opentelemetry.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package org.example.cloudnative.observability;

import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.api.trace.StatusCode;
import io.opentelemetry.api.trace.Tracer;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

public class CorrelatedSagaOrchestrator {
private final Tracer tracer;

public CorrelatedSagaOrchestrator(Tracer tracer) {
this.tracer = Objects.requireNonNull(tracer);
}

public void execute(String correlationId, List<CorrelatedSagaStep> steps) {
Span sagaSpan = tracer.spanBuilder("saga.orchestration")
.setSpanKind(SpanKind.INTERNAL)
.setAttribute("pattern.type", "saga")
.setAttribute("saga.correlation_id", correlationId)
.startSpan();

List<CorrelatedSagaStep> completed = new ArrayList<>();
try {
for (CorrelatedSagaStep step : steps) {
runStep(correlationId, step);
completed.add(step);
}
sagaSpan.setStatus(StatusCode.OK);
} catch (RuntimeException ex) {
rollback(correlationId, completed);
sagaSpan.recordException(ex);
sagaSpan.setStatus(StatusCode.ERROR, "saga failed");
throw new IllegalStateException("Correlated saga orchestration failed", ex);
} finally {
sagaSpan.end();
}
}

private void runStep(String correlationId, CorrelatedSagaStep step) {
Span span = tracer.spanBuilder("saga.step")
.setAttribute("saga.correlation_id", correlationId)
.setAttribute("saga.step", step.name())
.startSpan();
try {
step.action().accept(correlationId);
span.setStatus(StatusCode.OK);
} finally {
span.end();
}
}

private void rollback(String correlationId, List<CorrelatedSagaStep> completed) {
for (int i = completed.size() - 1; i >= 0; i--) {
CorrelatedSagaStep step = completed.get(i);
Span compensationSpan = tracer.spanBuilder("saga.compensation")
.setAttribute("saga.correlation_id", correlationId)
.setAttribute("saga.step", step.name())
.startSpan();
try {
step.compensation().accept(correlationId);
compensationSpan.setStatus(StatusCode.OK);
} finally {
compensationSpan.end();
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package org.example.cloudnative.observability;

import java.util.function.Consumer;

public record CorrelatedSagaStep(String name, Consumer<String> action, Consumer<String> compensation) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package org.example.cloudnative.observability;

import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.metrics.LongCounter;
import io.opentelemetry.api.metrics.Meter;
import org.example.enterprise.resilience.CircuitBreaker;

import java.util.Objects;
import java.util.function.Supplier;

public class ObservedCircuitBreaker {
private static final AttributeKey<String> OUTCOME = AttributeKey.stringKey("outcome");

private final CircuitBreaker delegate;
private final LongCounter executionCounter;

public ObservedCircuitBreaker(CircuitBreaker delegate, Meter meter) {
this.delegate = Objects.requireNonNull(delegate);
this.executionCounter = Objects.requireNonNull(meter)
.counterBuilder("pattern.circuit_breaker.executions")
.setDescription("Circuit breaker execution outcomes")
.build();
}

public <T> T execute(Supplier<T> operation) {
try {
T result = delegate.execute(operation);
executionCounter.add(1, Attributes.of(OUTCOME, "success"));
return result;
} catch (IllegalStateException openCircuit) {
executionCounter.add(1, Attributes.of(OUTCOME, "rejected"));
throw openCircuit;
} catch (RuntimeException failure) {
executionCounter.add(1, Attributes.of(OUTCOME, "failure"));
throw failure;
}
}
}
Loading
Loading