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

HeaderRequiredExampleNotes
AuthorizationYesBearer <token>Internal SSO token
Content-TypeYesapplication/jsonJSON requests
Idempotency-KeyRequired/Recommended (see endpoint)proj-req-7f3a...Safe retries / dedupe
X-Correlation-IdOptionalreq-12345Tracing 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

Request

Headers

HeaderRequiredExample
AuthorizationYesBearer <token>
Content-TypeYesapplication/json
Idempotency-KeyYescreate-proj-7f3a...
X-Correlation-IdOptionalreq-12345

Body (JSON)

FieldTypeRequiredNotes
namestringYesProject name
budgetCentslongYesBudget in cents
startDatestringYesYYYY-MM-DD
endDatestringYesYYYY-MM-DD, must be ≥ startDate
ownerIdstringYesEmployee ID
vendorIdstringNoOptional 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

ParamTypeRequiredNotes
projectIdstringYesProject identifier

Headers

HeaderRequiredExample
AuthorizationYesBearer <token>
Idempotency-KeyRecommendedapprove-proj-P-100245
X-Correlation-IdOptionalreq-99999

Body (JSON)

FieldTypeRequiredNotes
approvedBystringYesEmployee ID
approvedAtstringNoISO timestamp (server can fill)
commentstringNoOptional 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-Id is propagated to logs and downstream calls for tracing.
  • Error responses include traceId for faster debugging.
  • For async flows, monitor Kafka consumer lag and DLQ (if configured) to detect stuck contract creation.