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
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).