Skip to content
Open
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
27 changes: 19 additions & 8 deletions docs/api/events/order.cancelled.v1.json
Original file line number Diff line number Diff line change
@@ -1,17 +1,28 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://mini-commerce.local/schemas/order.cancelled.v1.json",
"title": "order.cancelled.v1",
"title": "order.cancelled.v1 envelope",
"type": "object",
"additionalProperties": false,
"required": ["orderId", "cancelledAt"],
"required": ["eventId", "type", "version", "occurredAt", "aggregateType", "aggregateId", "data"],
"properties": {
"orderId": { "type": "string", "format": "uuid" },
"cancelledAt": { "type": "string", "format": "date-time" },
"reason": { "type": ["string", "null"], "maxLength": 256 },
"eventId": { "type": "string", "format": "uuid" },
"type": { "type": "string", "const": "order.cancelled" },
"version": { "type": "string", "const": "1" }
"version": { "type": "string", "const": "v1" },
"occurredAt": { "type": "string", "format": "date-time" },
"aggregateType": { "type": "string", "const": "order" },
"aggregateId": { "type": "string", "format": "uuid" },
"traceId": { "type": "string" },
"data": {
"type": "object",
"additionalProperties": false,
"required": ["orderId", "cancelledAt"],
"properties": {
"orderId": { "type": "string", "format": "uuid" },
"cancelledAt": { "type": "string", "format": "date-time" },
"reason": { "type": ["string", "null"], "maxLength": 256 }
}
}
},
"description": "Canonical order.cancelled event (minimal) prior to envelope adoption."
"description": "Envelope + data payload for order.cancelled.v1 after transactional outbox adoption"
}

59 changes: 35 additions & 24 deletions docs/api/events/order.created.v1.json
Original file line number Diff line number Diff line change
@@ -1,34 +1,45 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://mini-commerce.local/schemas/order.created.v1.json",
"title": "order.created.v1",
"title": "order.created.v1 envelope",
"type": "object",
"additionalProperties": false,
"required": ["orderId", "customerId", "items", "currency", "total", "createdAt"],
"required": ["eventId", "type", "version", "occurredAt", "aggregateType", "aggregateId", "data"],
"properties": {
"orderId": { "type": "string", "format": "uuid" },
"customerId": { "type": "string", "format": "uuid" },
"items": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": ["sku", "name", "quantity", "unitPrice"],
"properties": {
"sku": { "type": "string", "minLength": 1 },
"name": { "type": "string", "minLength": 1 },
"quantity": { "type": "integer", "minimum": 1 },
"unitPrice": { "type": "number", "minimum": 0 }
"eventId": { "type": "string", "format": "uuid" },
"type": { "type": "string", "const": "order.created" },
"version": { "type": "string", "const": "v1" },
"occurredAt": { "type": "string", "format": "date-time" },
"aggregateType": { "type": "string", "const": "order" },
"aggregateId": { "type": "string", "format": "uuid" },
"traceId": { "type": "string" },
"data": {
"type": "object",
"additionalProperties": false,
"required": ["orderId", "customerId", "items", "currency", "total", "createdAt"],
"properties": {
"orderId": { "type": "string", "format": "uuid" },
"customerId": { "type": "string", "format": "uuid" },
"items": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": ["sku", "name", "quantity", "unitPrice"],
"additionalProperties": false,
"properties": {
"sku": { "type": "string", "minLength": 1 },
"name": { "type": "string", "minLength": 1 },
"quantity": { "type": "integer", "minimum": 1 },
"unitPrice": { "type": "number", "minimum": 0 }
}
}
},
"additionalProperties": false
"currency": { "type": "string", "pattern": "^[A-Z]{3}$" },
"total": { "type": "number", "minimum": 0 },
"createdAt": { "type": "string", "format": "date-time" }
}
},
"currency": { "type": "string", "pattern": "^[A-Z]{3}$" },
"total": { "type": "number", "minimum": 0 },
"createdAt": { "type": "string", "format": "date-time" },
"type": { "type": "string", "const": "order.created", "description": "(Future envelope field)" },
"version": { "type": "string", "const": "1", "description": "(Future envelope field)" }
}
},
"description": "Canonical order.created event (current minimal payload; envelope fields optional until outbox/envelope rollout)."
"description": "Envelope + data payload for order.created.v1 after transactional outbox adoption"
}

146 changes: 146 additions & 0 deletions docs/task-status-template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# 🛠 Task Status Template

> Timestamp: {{YYYY-MM-DD}}
> Owner: {{Name / Squad}}
> Objective: {{Concise, outcome-focused statement}}

---
## 🎯 Why This Matters
Brief business / architectural justification. What risk or gap are we closing? What value / narrative (portfolio, reliability, etc.) is improved?

---
## 🗺 High-Level Plan
1. {{Step 1}}
2. {{Step 2}}
3. {{Step 3}}
4. {{Stretch / Optional}}

---
## 🧱 Scope (In / Out)
| Area | In Scope | Deferred |
|------|----------|----------|
| Reliability | {{Yes}} | {{Out-of-scope items}} |
| Schema / Contracts | {{}} | {{}} |
| Observability | {{}} | {{}} |
| Domain Model | {{}} | {{}} |
| Resilience | {{}} | {{}} |
| Ops / Admin | {{}} | {{}} |

---
## 📦 Deliverables Status
List concrete artifacts (code units, docs, migrations). Use emoji legend below.
- 🟢 {{Example Deliverable Done}}
- 🟡 {{In Progress Deliverable}}
- 🔴 {{Not Started Deliverable}}

---
## ✅ Status Legend
| Emoji | Meaning |
|-------|---------|
| 🟢 | Done |
| 🟡 | In Progress / Partial |
| 🔴 | Not Started |
| 🧪 | Testing / Verifying |
| 📌 | Blocked / Decision Needed |

---
## 📋 Task Board
### 1. {{Category A}}
- 🔴 {{Task}}
- 🟡 {{Task}}

### 2. {{Category B}}
- 🔴 {{Task}}

### 3. {{Category C}}
- 🔴 {{Task}}

(Replicate categories as needed: Core Impl, Reliability, Domain, Tests, Metrics, Docs, Stretch)

---
## 🧪 Test Matrix
| Test Name | Category | Purpose | Status |
|-----------|----------|---------|--------|
| {{test_create}} | Integration | Ensures X | 🔴 |
| {{test_validation}} | Unit | Rejects invalid input | 🔴 |
| {{test_retry}} | Integration | Retry logic works | 🔴 |

---
## 🔐 Design Notes
Bullets summarizing patterns / constraints:
- Pattern: {{Transactional Outbox / Saga / CQRS}}
- Backoff: {{formula}}
- Serialization: {{JSON / Avro}}
- Idempotency: {{Key derivation}}

---
## ⚠ Risks & Mitigations
| Risk | Impact | Mitigation | Owner |
|------|--------|-----------|-------|
| {{Risk}} | {{High/Med/Low}} | {{Mitigation}} | {{}} |

---
## 📊 Metrics (Planned)
List metric names early for consistency.
- {{service.outbox.pending}}
- {{service.latency.ms}}
- {{domain.event.publish.attempt{status}}}

---
## 🧵 Trace & Correlation
How traceId / correlationId flows, any headers, propagation gaps.

---
## ⏭ Next Sprint Focus
1. {{Immediate next actionable}}
2. {{Secondary}}
3. {{Stretch}}

---
## 🔄 Progress Log
| Time | Action | Notes |
|------|--------|-------|
| {{YYYY-MM-DD}} | Created | Initial scaffold |
| {{YYYY-MM-DD}} | Update | {{Milestone}} |

---
## 📥 Dependencies / Libraries
- {{lib:name}} (purpose)

---
## 🧭 Acceptance Criteria
| Criterion | Status | Evidence |
|-----------|--------|----------|
| {{No afterCommit}} | 🔴 | {{Link / Test}} |
| {{Retry backoff}} | 🔴 | {{Test name}} |
| {{Contract validated}} | 🔴 | {{Schema test}} |

---
## 📝 Example Envelope (If Event Work)
```json
{
"eventId": "...",
"type": "...",
"version": "v1",
"occurredAt": "...",
"aggregateType": "...",
"aggregateId": "...",
"traceId": "...",
"data": {"...": "..."}
}
```

---
## 📎 Pending Decisions
| Topic | Needed By | Options | Chosen | Notes |
|-------|-----------|---------|--------|-------|
| {{Schema registry?}} | {{Date}} | {{A/B/C}} | {{?}} | {{}} |

---
## 📚 Appendix (Optional)
- Sequence Diagram: `docs/diagrams/{{name}}.puml`
- ADR References: `docs/adr/{{id}}-*.md`

---
Fill placeholders, prune unused sections, and keep concise. Keep this template versioned for consistency across services.

74 changes: 74 additions & 0 deletions order-service/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
# Order Service

[![CI](https://github.com/misterquestions/mini-commerce/actions/workflows/ci.yml/badge.svg)](https://github.com/ORG_NAME/mini-commerce/actions/workflows/ci.yml)

The **Order Service** is the authoritative source for order management in Mini-Commerce. It owns the order domain, enforces status transitions, and reliably publishes domain events to Kafka using the transactional outbox pattern.

> OpenAPI: See the generated specification at [`src/main/resources/openapi/order-service.yaml`](src/main/resources/openapi/order-service.yaml)

## Purpose & Responsibilities
- **Owns**: Orders, status machine
- **Publishes**: order.created, order.paid, order.fulfillment_requested, order.cancelled, order.completed
Expand Down Expand Up @@ -53,6 +57,76 @@ Events are published to Kafka using a **transactional outbox** pattern:
- ![System Overview](../docs/diagrams/image/system-overview.png)
- [OpenAPI Spec](src/main/resources/openapi/order-service.yaml)

## Event Envelope (Implemented)
Domain events are emitted via the transactional outbox as an EventEnvelope structure:

```
{
"eventId": "<uuid>",
"type": "order.created",
"version": "v1",
"occurredAt": "2025-09-09T10:00:00Z",
"aggregateType": "order",
"aggregateId": "<order-uuid>",
"traceId": "<trace-uuid>",
"data": {
"orderId": "<order-uuid>",
"customerId": "<customer-uuid>",
"currency": "USD",
"total": 39.98,
"items": [ { "sku": "SKU1", "name": "Mouse", "quantity": 2, "unitPrice": 19.99 } ],
"createdAt": "2025-09-09T10:00:00Z"
}
}
```

## Metrics (Micrometer)
- `orders.outbox.pending`
- `orders.outbox.failed`
- `orders.outbox.backlog.oldest_age_seconds`
- `orders.outbox.relay.batch.duration` (timer)
- `orders.outbox.publish.attempt{status=success|failure}`

## Health & Monitoring
The service exposes Spring Boot Actuator health at:

```
GET /actuator/health
```
Example (DEGRADED when failed outbox rows exist):
```
{
"status": "DEGRADED",
"components": {
"outboxHealthIndicator": {
"status": "DEGRADED",
"details": {
"outbox.pending": 12,
"outbox.failed": 2,
"outbox.oldestAgeSeconds": 187
}
}
}
}
```
When there are no failed events the status returns `UP` and failed count is 0.

Curl quick check:
```
curl -s http://localhost:8080/actuator/health | jq
```

Key fields:
- `outbox.pending`: total NEW + RETRY events waiting publishing
- `outbox.failed`: events that exhausted max attempts (operational action required)
- `outbox.oldestAgeSeconds`: age of the oldest pending event, useful for alert thresholds

## Admin Endpoints (Outbox Operations)
| Method | Path | Description |
|--------|------|-------------|
| POST | /api/v1/outbox/requeue-failed | Requeue all FAILED events (status -> RETRY) |
| POST | /api/v1/outbox/{id}/requeue | Requeue single event by id |

---

For questions, see the global [README](../README.md) or reach out to the team.
3 changes: 3 additions & 0 deletions order-service/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.kafka:spring-kafka")
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0")
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("com.networknt:json-schema-validator:1.0.86")

runtimeOnly("org.postgresql:postgresql")
implementation("org.flywaydb:flyway-core")
Expand All @@ -34,6 +36,7 @@ dependencies {
testImplementation("org.flywaydb:flyway-core")
testRuntimeOnly("org.postgresql:postgresql")
testImplementation("org.apache.httpcomponents.client5:httpclient5:5.3.1")
testImplementation("org.awaitility:awaitility:4.2.0")
}

tasks.test {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
package com.minicommerce.orders;

import com.minicommerce.orders.outbox.OutboxProperties;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling
@EnableConfigurationProperties(OutboxProperties.class)
public class OrdersApplication {
public static void main(String[] args) {
SpringApplication.run(OrdersApplication.class, args);
Expand Down
Loading