Layer 2 — API Gateway: Spring Boot (GraphQL + REST)

Two distinct APIs — a GraphQL BFF for the workspace, and a REST API for mobile and external channels — backed by nine domain services.


Table of Contents

  1. 5.1 Two-API Pattern
  2. 5.2 Domain Service Decomposition
  3. 5.3 GraphQL Schema
  4. 5.4 API → Temporal Bridge

5.1 Two-API Pattern

API Transport Consumers Pattern
Workspace BFF GraphQL over HTTP React web, AI Assistant panel Backend-for-Frontend — flexible queries, role-aware field visibility
External API REST (OpenAPI 3.0) React Native, Digital Account Opening Channel, external intake Standard REST, versioned endpoints

Both APIs are Spring Boot applications (spring-boot-starter-web + spring-boot-starter-graphql).


5.2 Domain Service Decomposition

Spring Boot is used for multiple distinct services, not a monolith. Each service owns its domain:

DOMAIN SERVICES

case-service           — CRUD for onboarding cases              [Java / Spring Boot]
product-service        — product catalog, selection rules        [Java / Spring Boot]
workflow-service       — Temporal client bridge                  [Java / Spring Boot]
integration-service    — external system adapters (Layer 5)      [Java / Spring Boot]
document-service       — S3 upload/download, metadata            [Java / Spring Boot]
notification-service   — real-time WebSocket hub + email/push    [Go]
user-service           — operators, roles, authentication        [Java / Spring Boot]
audit-service          — immutable audit trail                   [Java / Spring Boot]
metrics-service        — SLA computation, dashboard feeds        [Java / Spring Boot]

Why notification-service is Go: This service maintains thousands of persistent WebSocket connections to React workspace clients — one per open browser tab. Go goroutines handle this concurrency model (many idle connections, infrequent writes) at ~2KB stack per goroutine vs ~1MB per OS thread. A Spring Boot equivalent would either require reactive/WebFlux (complex) or exhaust its thread pool under load.


5.3 GraphQL Schema

The workspace BFF exposes a GraphQL schema that maps directly to the case model and workflow state:

type Case {
  id: ID!
  caseNumber: String!
  clientSegment: ClientSegment!
  stage: WorkflowStage!
  company: CompanyInfo!
  products: [ProductSelection!]!
  users: [UserRecord!]!
  accounts: [AccountRecord!]!
  documents: [DocumentRecord!]!
  workflowState: WorkflowState!
  auditEvents: [AuditEvent!]!
  aiSuggestions: [AISuggestion!]!
}

type WorkflowState {
  currentPhase: String!
  activeTask: WorkItem
  pendingSignals: [SignalDefinition!]!
  runId: String!
}

enum ClientSegment {
  SMALL
  MEDIUM
  ENTERPRISE
}

type Query {
  myQueue(filter: QueueFilter): [Case!]!
  teamQueue(groupId: ID!, filter: QueueFilter): [Case!]!
  case(id: ID!): Case
  dashboardMetrics(window: MetricsWindow!): DashboardData!
}

type Mutation {
  createCase(input: CaseInput!): Case!
  submitApproval(caseId: ID!, decision: ApprovalDecision!, comments: String): Case!
  uploadDocument(caseId: ID!, type: DocumentType!): UploadToken!
  triggerErrorRecovery(caseId: ID!, action: RecoveryAction!): Case!
  askAIAssistant(caseId: ID!, question: String!): AIAssistantResponse!
}

type Subscription {
  caseUpdated(caseId: ID!): CaseUpdate!
}

Role-aware field visibility: Spring Security + GraphQL field visibility ensures that fields are filtered by the operator’s role. A Document Review Analyst sees document assessment fields; a Compliance Officer sees AML questionnaire fields; a Manager sees metrics fields — all from the same Case type, filtered at the resolver layer.


5.4 API → Temporal Bridge

The workflow-service acts as the bridge between the API layer and Temporal. It wraps all Temporal client operations (start workflow, send signal, query workflow state):

@Service
public class WorkflowService {

    private final WorkflowClient temporalClient;

    public CaseStatus startOnboarding(OnboardingRequest request) {
        ClientServicingWorkflow workflow = temporalClient.newWorkflowStub(
                ClientServicingWorkflow.class,
                WorkflowOptions.newBuilder()
                        .setTaskQueue("onboarding-task-queue")
                        .setWorkflowId("case-" + request.getCaseId())
                        .build()
        );
        WorkflowClient.start(workflow::execute, request);
        return CaseStatus.INITIATED;
    }

    public void sendApprovalSignal(String caseId, ApprovalDecision decision) {
        ClientServicingWorkflow workflow = temporalClient.newWorkflowStub(
                ClientServicingWorkflow.class, "case-" + caseId
        );
        workflow.submitApproval(decision);
    }
}

Workflow ID strategy: Each onboarding case maps to exactly one Temporal Workflow Execution, identified by "case-" + caseId. This means the API layer can always reach the correct workflow execution by case ID — no need to store the run ID separately (the case ID is the stable identifier).


↑ Back to top

Anchita Platform — Fictional Reference Architecture for Cloud-Native Institutional Banking