When I migrated a monolith to microservices, I designed the integration in two layers: synchronous REST for request/response, and Kafka events for cross-service workflows.
For the REST APIs, I focused on clear service boundaries and stable contracts.
Each service owned its own data and exposed APIs that matched business actions.
I used consistent endpoint naming, input validation, and error handling.
I always set timeouts and limit retries to avoid cascading failures.
For write operations, I designed idempotency so a retry would not create duplicates. I also thought about versioning and backward compatibility, so services could deploy independently.
For Kafka, I used events mainly for cross-service workflows where decoupling mattered.
I treated events like contracts too:
- clear schema,
- versioning,
- a stable event name.
I assumed at-least-once delivery, so consumers had to be idempotent, and I added monitoring and alerts for consumer lag and failures. I also made sure we had good tracing—like correlation IDs—so we could follow a request across REST calls and async events.
Overall, my goal was to keep the default path simple, and add async events only where it improved reliability and made future integrations easier, without creating operational risk.
ProjectManagerService API Documentation (Sample)
Purpose: Provide a clear REST API contract for creating and approving projects.
Async Integration: After approval, ProjectManagerService publishes a Kafka event that is consumed by ContractService to create a contract asynchronously.
Conventions
Authentication
- Auth: Internal SSO (OAuth2/JWT)
- Header:
Authorization: Bearer <token>
Common Headers
| Header | Required | Example | Notes |
|---|---|---|---|
Authorization | Yes | Bearer <token> | Internal SSO token |
Content-Type | Yes | application/json | JSON requests |
Idempotency-Key | Required/Recommended (see endpoint) | proj-req-7f3a... | Safe retries / dedupe |
X-Correlation-Id | Optional | req-12345 | Tracing across services |
Standard Error Format
{
"errorCode": "SOME_CODE",
"message": "Human readable message",
"traceId": "abc-123"
}Key Status Codes (Summary)
- 201 Created: Resource created (sync)
- 202 Accepted: Request accepted, processing is async (Kafka)
- 400: Invalid request (validation)
- 401/403: Auth / permission
- 404: Not found
- 409: Conflict (idempotency/business conflict)
- 429: Too many requests (rate limit)
- 500/503: Server error / unavailable
1) Create Project
Overview
Create a new project in ProjectManagerService.
Returns a projectId. Contract creation (if any) happens later after approval via Kafka.
Endpoint
- Method:
POST - Path:
/v1/projects
Authorization
- Required Scope/Role:
project:write
Idempotency
- Header:
Idempotency-Key(required) - Behavior:
- Same
Idempotency-Key+ same payload → return the same result (safe retry) - Same
Idempotency-Key+ different payload → 409 Conflict
- Same
Request
Headers
| Header | Required | Example |
|---|---|---|
Authorization | Yes | Bearer <token> |
Content-Type | Yes | application/json |
Idempotency-Key | Yes | create-proj-7f3a... |
X-Correlation-Id | Optional | req-12345 |
Body (JSON)
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | Yes | Project name |
budgetCents | long | Yes | Budget in cents |
startDate | string | Yes | YYYY-MM-DD |
endDate | string | Yes | YYYY-MM-DD, must be ≥ startDate |
ownerId | string | Yes | Employee ID |
vendorId | string | No | Optional vendor reference |
Example Request
{
"name": "Vendor Onboarding Phase 1",
"budgetCents": 5000000,
"startDate": "2026-03-01",
"endDate": "2026-06-01",
"ownerId": "E10293",
"vendorId": "V7788"
}Responses
✅ 201 Created
When: Project is created successfully.
Example Response
{
"projectId": "P-100245",
"status": "DRAFT",
"createdAt": "2026-02-16T19:12:45Z"
}Errors
400 Bad Request
When: Missing/invalid fields (e.g., date range invalid).
{
"errorCode": "INVALID_ARGUMENT",
"message": "endDate must be greater than or equal to startDate",
"traceId": "abc-123"
}401 Unauthorized
When: Missing/invalid token.
{
"errorCode": "UNAUTHORIZED",
"message": "Missing or invalid token",
"traceId": "abc-124"
}403 Forbidden
When: Authenticated but lacks permission (project:write).
{
"errorCode": "FORBIDDEN",
"message": "Insufficient permissions",
"traceId": "abc-125"
}409 Conflict
When: Idempotency conflict (same key, different payload).
{
"errorCode": "IDEMPOTENCY_CONFLICT",
"message": "Idempotency-Key reused with different request payload",
"traceId": "abc-126"
}500 Internal Server Error
{
"errorCode": "INTERNAL_ERROR",
"message": "Unexpected server error",
"traceId": "abc-127"
}2) Approve Project (Async → Kafka)
Overview
Approve an existing project. This is the trigger point for asynchronous contract creation.
ProjectManagerService will publish a Kafka event PROJECT_APPROVED to topic project-events.
ContractService subscribes to that topic and creates the contract asynchronously.
Endpoint
- Method:
POST - Path:
/v1/projects/{projectId}/approve
Authorization
- Required Scope/Role:
project:approve
Idempotency
- Header:
Idempotency-Key(recommended) - Behavior:
- Safe to retry without approving twice
- If already approved, return 200 OK (or 202 Accepted) with current state
Request
Path Parameters
| Param | Type | Required | Notes |
|---|---|---|---|
projectId | string | Yes | Project identifier |
Headers
| Header | Required | Example |
|---|---|---|
Authorization | Yes | Bearer <token> |
Idempotency-Key | Recommended | approve-proj-P-100245 |
X-Correlation-Id | Optional | req-99999 |
Body (JSON)
| Field | Type | Required | Notes |
|---|---|---|---|
approvedBy | string | Yes | Employee ID |
approvedAt | string | No | ISO timestamp (server can fill) |
comment | string | No | Optional approval comment |
Example Request
{
"approvedBy": "E20418",
"comment": "Approved for contract initiation"
}Responses
✅ 202 Accepted
When: Approval is accepted and contract creation will happen asynchronously.
Example Response
{
"projectId": "P-100245",
"status": "APPROVED",
"contractStatus": "PENDING",
"requestId": "R-778899",
"message": "Approval accepted; contract creation is queued"
}✅ 200 OK (Idempotent replay)
When: Project was already approved (safe retry).
Example Response
{
"projectId": "P-100245",
"status": "APPROVED",
"contractStatus": "PENDING",
"message": "Project already approved"
}Kafka Event Published
Topic
project-events
Event Type
PROJECT_APPROVED
Event Payload (Example)
{
"eventId": "e-123",
"eventType": "PROJECT_APPROVED",
"occurredAt": "2026-02-16T20:01:02Z",
"projectId": "P-100245",
"vendorId": "V7788",
"budgetCents": 5000000,
"startDate": "2026-03-01",
"endDate": "2026-06-01",
"approvedBy": "E20418",
"idempotencyKey": "P-100245:PROJECT_APPROVED"
}Errors
400 Bad Request
When: Project state is invalid for approval (e.g., missing required fields).
{
"errorCode": "INVALID_STATE",
"message": "Project cannot be approved in current state",
"traceId": "def-201"
}401 Unauthorized
{
"errorCode": "UNAUTHORIZED",
"message": "Missing or invalid token",
"traceId": "def-202"
}403 Forbidden
{
"errorCode": "FORBIDDEN",
"message": "Insufficient permissions",
"traceId": "def-203"
}404 Not Found
When: projectId does not exist.
{
"errorCode": "PROJECT_NOT_FOUND",
"message": "Project not found",
"traceId": "def-204"
}409 Conflict
When: Approval conflicts with business rules (optional).
{
"errorCode": "APPROVAL_CONFLICT",
"message": "Approval conflicts with current project state",
"traceId": "def-205"
}500 / 503
{
"errorCode": "INTERNAL_ERROR",
"message": "Unexpected server error",
"traceId": "def-206"
}Notes (Operational / Observability)
X-Correlation-Idis propagated to logs and downstream calls for tracing.- Error responses include
traceIdfor faster debugging. - For async flows, monitor Kafka consumer lag and DLQ (if configured) to detect stuck contract creation.