1 GCP AI stack & the document-agent
The AI/ML layer of the build. Name services correctly and you erase "average" fast.
• "Bulk per-page extraction goes to a Flash-tier Gemini for cost/latency; Pro for adjudication-grade reasoning; everything grounded so each answer carries a citation."
• "For a regulated insurer I default to high-fidelity grounding — the model answers strictly from our documents — and keep Google-Search grounding off the adjudication path."
The GCP AI services
- Vertex AI → now the Gemini Enterprise Agent Platform. One control plane for models + agents + governance.
- Document AI — Enterprise OCR (+handwriting), Custom Document Extractor, Layout Parser (chunks), Workbench (uptraining).
- Vertex AI Search — managed enterprise RAG out of the box.
- Vertex AI Vector Search — managed ANN index (fmr Matching Engine).
- Agent Builder + ADK + Agent Engine — code-first agents (ADK), managed runtime; tools over MCP.
- Gen AI Evaluation Service — pointwise/pairwise + agent-trajectory scoring.
- High-fidelity grounding — answers strictly from provided context.
Two ground-truth distinctions
- Gemini Enterprise (fmr Agentspace) = employee-facing search/agent workspace — CURE's "Gemini for all."
- Gemini Enterprise Agent Platform (fmr Vertex AI) = the developer/build layer. Don't conflate.
- Never quote exact model IDs, context maxima, or GA status as fact — say the capability, then "verify in your tenant."
Walk us through the end-to-end agent architecture for a 1,000-page claims PDF.
- Ingest + split — PDF to GCS, shard under Document AI's ~200pp cap, track page offsets.
- Extract — Document AI CDE + OCR + Layout Parser (crash diagram stays tied to its page).
- Compose — Gemini assembles the narrative against versioned business rules, citing source pages.
- Validate — a separate evaluator scores it before any human sees it.
- Durable workflow, not an in-memory loop — a failed page retries without redoing the file.
- Every stage writes to a central repo with audit trails + versioning.
Why a separate validation agent instead of trusting the extractor?
- Validator checks three failure classes: missing fields, bad chronology, numbers that don't reconcile.
- Grade it against a labeled gold set so we know its false-negative rate.
- Doesn't replace the human — it triages: reviewers see the ~10-15% flagged low-confidence, not all 100.
- "AI informs, humans decide" applied to throughput.
How do you keep agents grounded in CURE's business rules, not the model's priors?
- Externalize them as a rules layer the agent must reference.
- Deterministic rules → enforce in code, don't ask the model to remember.
- Model does language + synthesis; the rules engine does the checking.
- Auditable: "the LLM felt like it" won't pass your board — you can show where a decision came from.
- Version rules with the code, so a change lets you re-run old claims and see what differs.
How do you handle images — crash diagrams, handwriting, photos?
- Layout Parser + OCR give bounding boxes + page anchors — "crash diagram, p.214" stored with the field it supports.
- Handwriting via Enterprise OCR at lower confidence → carries a score, likelier to route to review.
- A low-confidence handwritten number never flows silently into a settlement figure.
- Gemini multimodal can look at an ambiguous image when OCR is unsure — an assist, not ground truth.
- Every attached image is clickable back to its source page.
How do you prevent hallucination writing a chronology from a 1,000-page file?
- Grounding — high-fidelity mode: every claim maps to a source span, no free-writing.
- Citations + confidence — every field shows source page + a confidence bar (built into askthedeclaration.com).
- Validation agent + human-in-the-loop on settlement-affecting decisions.
- The goal is to make hallucination visible and catchable, not to claim it's gone.
What's your eval strategy? How do you know it's good enough to ship?
- Gold set of real de-identified claims, hand-annotated by SMEs.
- Score field-level precision/recall + chronology-order accuracy.
- Gen AI Evaluation Service gives computational + model-graded metrics.
- Validate the LLM-judge against human labels first — on askthedeclaration.com I had real lawyers validate it; an unvalidated judge is just another hallucination.
- Every model/prompt change runs the suite before shipping; numbers go to the board.
Managed Vertex AI Search vs building your own chunking/embeddings/vector store?
- 4-5 devs, no time for plumbing → Vertex AI Search / Vector Search = fast baseline, low ops.
- But I've built RAG by hand (chunking, embeddings, index, retrieval) on my constitutional-docs projects, so I know what the managed layer hides.
- Long medical records benefit from context-aware chunking on section/date boundaries.
- Rule: managed until we hit a wall on quality or cost, then selectively customize.
How do you evaluate retrieval separately from generation?
- Retrieval: recall@k / precision on a labeled "these pages answer this" set.
- Generation: given correct context, score faithfulness to what was retrieved.
- If the right pages don't come back, no prompt tuning fixes the narrative.
- The split tells you: retrieval miss or reasoning miss — a five-minute fix vs a week of prompt roulette.
⭐ Document AI caps ~200 pages. Your files are 800-1,000. What now?
- Chunk into sub-200-page segments (verify the cap in your tenant), process in parallel.
- Track page offsets + anchors so every field still points to its true page in the original.
- A reassembly step stitches segments into one coherent claim.
- Chronology is built across segment boundaries — a timeline doesn't care where the split landed.
- Overlap segment edges + reconcile — same instinct as overlapping RAG chunks.
The model gets a settlement figure wrong and an adjuster relies on it — who's accountable?
- Settlement numbers carry confidence + cite their exact source line item.
- The validation agent scrutinizes them and routes low-confidence figures to review — never silently to an adjuster.
- Audit trail records what the model produced, what it cited, and who approved it.
- The AI is never the final signer on a payout — that's a governance failure and, in insurance, bad-faith exposure.
2 Databases & SQL
Your home turf. 15 years of SQL is an asset — model it as a normalized relational core + append-only audit/version log + explicit entitlement layer.
Where data lives
- Cloud SQL / AlloyDB (Postgres) — the transactional system of record. AlloyDB adds pgvector + ScaNN, so retrieval vectors sit next to relational data and can be entitlement-filtered in the same query.
- BigQuery — analytics/reporting + the Redshift-migration target; vector search + BQML in-warehouse. Not the OLTP store.
- Spanner — global strong consistency; say you would not reach for it here (knowing when not to use a service is the senior signal).
Schema spine to sketch
organizations · users · cases · documentsdocument_versions(version = new row, never overwrite)document_pages(models the ~200pp split)extractions(who/what/when, tied to a version)extracted_fields · medical_events · settlement_values(each with source_page + confidence)audit_log(append-only) ·entitlements(access is data, not code)
-- Latest version of each document (window fn, no self-join)
SELECT case_id, document_id, version_id, version_no, gcs_uri
FROM (
SELECT d.*, ROW_NUMBER() OVER (
PARTITION BY document_id ORDER BY version_no DESC) AS rn
FROM document_versions d )
WHERE rn = 1; -- BigQuery/Snowflake: use QUALIFY to skip the subquery
-- Medical chronology by content-derived date, with gaps between events
SELECT case_id, event_date, event_type, provider, description,
DATE_DIFF(event_date,
LAG(event_date) OVER (PARTITION BY case_id ORDER BY event_date),
DAY) AS days_since_prev
FROM medical_events
WHERE case_id = @case_id
ORDER BY event_date; -- Postgres/AlloyDB: event_date - LAG(...) instead of DATE_DIFF
-- Audit: who extracted what, in a date range
SELECT u.email, d.source_filename, e.model_version, e.extracted_at,
COUNT(f.field_id) AS fields_extracted
FROM extractions e
JOIN users u ON u.user_id = e.extracted_by
JOIN document_versions dv ON dv.version_id = e.document_version_id
JOIN documents d ON d.document_id = dv.document_id
LEFT JOIN extracted_fields f ON f.extraction_id = e.extraction_id
WHERE e.extracted_at >= '2026-01-01' AND e.extracted_at < '2026-02-01'
GROUP BY u.email, d.source_filename, e.model_version, e.extracted_at
ORDER BY e.extracted_at;
Naming QUALIFY, row-level security, and pre-filtering the vector search reads as fluent-in-warehouse. Never SELECT * on PII/medical tables; think execution plan, not just result.
- Inventory before you move anything. Every table, view, stored proc, UDF, and every scheduled/undocumented job. "The risk isn't the tables you know — it's the stored proc that only runs Dec 31 for one client. Those show up in query history and cron, not a schema dump."
- The move: BigQuery Migration Service (assessment + SQL translation, ~95% of queries; stored procs are the manual tail). Transfer routes Redshift → S3 unload → BigQuery. CDC during cutover = Airbyte (Datastream can't read Redshift).
- Latency (their motivation): partition + cluster; BI Engine + materialized views; move latency-sensitive operational reads off the warehouse to AlloyDB — a warehouse is not a point-lookup engine, which is often the real "Redshift is slow."
- No-deletion policy: tier cold data to GCS + external tables, don't expire; frame immutable history as a compliance asset. Run old + new in parallel, reconcile, cut over per-consumer — never big-bang. Don't overpromise a timeline before seeing the proc/job inventory.
How do you model the central repo — narratives, extractions, audit, versioning?
- Claim → documents → extracted fields → source-page provenance + confidence; everything has a version + an actor.
- Tables: claims, documents, extractions, narrative-versions, + an append-only audit table.
- Versioning = "new row, never overwrite"; audit immutable by construction.
- AlloyDB keeps pgvector + ScaNN in the same DB — embeddings next to relational data.
- BigQuery = analytics side, kept separate. 15 yrs SQL → I own schema, indexing, migration.
AlloyDB pgvector vs Vertex AI Vector Search — where does vector search live?
- AlloyDB (pgvector/ScaNN) — vectors next to relational data; one system, transactional consistency; filter by access control in the same query as the similarity search.
- Vertex AI Vector Search — specialized, massively scalable, fully managed, decoupled.
- For 50-100 users I'd lean AlloyDB first for simplicity + inline access control, reserving Vertex for if we outgrow it.
⭐ 50-100 users, per-user access control — how do you enforce it?
- Enforce entitlement at the data layer, not the UI — every query scoped by identity + role.
- Postgres row-level security so an app bug can't bypass it.
- Identity from the IdP via IAM (Entra SSO + entitlement layer at Prudential; Workspace/IAM here).
- RAG too: filter the vector search to permitted claims before results return, or you leak through the AI.
- Every access is logged.
The Redshift migration, given Datastream can't source Redshift?
- Path: unload Redshift → S3 → GCS → BigQuery; Airbyte for ongoing incremental sync.
- Datastream earns its keep after cutover — CDC from operational Postgres/AlloyDB into BigQuery.
- Run old + new in parallel; reconcile row counts + key aggregates before flipping anything off.
- Validate with data, not optimism. Sequenced after the claims platform, but design the schema now.
3 Infrastructure & setup the flagged gap
Convert "I do this on AWS" into "on GCP that's X" without hesitating. These details are what AWS/Azure people routinely miss.
Fundamentals not to fumble
- Hierarchy: Org → Folder → Project → Resource. The Project is billing + quota + IAM boundary; policy inherits downward only.
- IAM = member + role + resource (a binding). Predefined/custom roles, never basic Owner/Editor in prod. Service accounts are identities workloads run as.
- Keyless auth: Workload Identity Federation / GKE Workload Identity. Avoid downloadable SA keys.
- Regions vs zones: zone ≈ AZ; subnets regional, VPC global.
- VPC-SC = exfiltration perimeter. CMEK = your keys. Org Policy = the SCP analog.
- Cloud Audit Logs: Admin Activity always on; Data Access OFF by default — enable + sink to a locked logging project.
"Stand up a secure environment" — senior checklist
- Everything IaC: Terraform + Infrastructure Manager (not Deployment Manager).
- Folders by env/sensitivity; separate dev/stage/prod + dedicated logging & security projects.
- Federated keyless identity; least-privilege IAM; Org Policy guardrails.
- Shared VPC, regional subnets, no public IPs, egress via Cloud NAT allowlist; Cloud Armor on external LBs.
- VPC-SC perimeter around Vertex AI + BigQuery + GCS + Cloud SQL.
- CMEK everywhere; secrets in Secret Manager.
- Enable Data Access logs → locked logging project; Security Command Center for posture.
- Billing budgets + quotas to cap runaway inference spend.
| Concept | AWS | GCP | Note |
|---|---|---|---|
| Account boundary | Account / Org / OU | Project / Org / Folder | Project = billing + IAM + quota unit |
| Serverless containers | ECS / Fargate | Cloud Run | Default "run a container"; scales to zero |
| Kubernetes | EKS | GKE (Autopilot = managed nodes) | |
| Functions | Lambda | Cloud Run functions | Fmr Cloud Functions (renamed 2024) |
| Load balancer | ALB / NLB | Cloud Load Balancing | Global anycast is the differentiator |
| Private service access | PrivateLink | Private Service Connect | |
| Identity federation | STS / IAM roles | Workload Identity Federation | Keyless, short-lived creds |
| Guardrails | SCP | Org Policy | Region lock, no external IP, enforce CMEK |
| Exfil perimeter | (VPC endpoints) | VPC Service Controls | Stronger than PrivateLink alone |
| Audit trail | CloudTrail | Cloud Audit Logs | Data Access logs off by default |
| WAF / DDoS | WAF / Shield | Cloud Armor | On the global LB |
| Secrets / keys | Secrets Mgr / KMS | Secret Manager / Cloud KMS | CMEK; EKM for external HSM custody |
| Observability | CloudWatch / X-Ray | Cloud Monitoring & Logging / Cloud Trace | |
| Events / messaging | EventBridge / SNS+SQS | Eventarc / Pub/Sub | Pub/Sub covers both SNS + SQS |
| Orchestration | Step Functions | Workflows / Cloud Composer | Composer = managed Airflow |
| Warehouse | Redshift | BigQuery | Partition/cluster replace DISTKEY/SORTKEY |
| Relational OLTP | RDS / Aurora | Cloud SQL / AlloyDB | AlloyDB = Aurora analog + ScaNN vectors |
| CDC / migration | DMS | Datastream | No Redshift source — use Airbyte |
| Doc extraction | Textract | Document AI | More Gemini-native |
| Managed RAG / search | Kendra | Vertex AI Search | Your Prudential Kendra story maps here |
| Vector index | OpenSearch kNN | Vertex AI Vector Search | Fmr Matching Engine |
| CI/CD & IaC | CodePipeline / CloudFormation | Cloud Build / Infrastructure Manager | Deployment Manager deprecated |
Set up the security & isolation posture — network, keys, identity.
- VPC Service Controls — perimeter so claims data can't exfiltrate.
- CMEK — CURE holds + can revoke the keys (your board wants this for medical data).
- IAM least-privilege — service accounts per stage, humans via Workspace SSO, no standing broad roles.
- GCS: CMEK-encrypted, uniform bucket-level access, no public exposure.
- I'd verify VPC-SC coverage for Document AI + Vertex in your tenant — I won't claim coverage I haven't confirmed.
How do you orchestrate, deploy, and monitor it?
- Everything Terraform — reproducible environments.
- Long jobs = a durable workflow (Workflows / Agent Engine), retryable per-page + idempotent.
- Observability: structured logging, Cloud Monitoring, traces across stages, alerts on latency / error rate / cost-per-document / eval-score drift.
- At Prudential I picked Kendra partly for logging + observability — a system you can't see into is one you can't govern.
Naming has shifted — are you current?
- Cloud Next '26: Vertex AI agent stack → Gemini Enterprise Agent Platform.
- Agentspace → Gemini Enterprise (Oct 2025) — the Workspace search/agent surface, fits your "Gemini for all."
- The primitives I build on (Document AI, Vector Search, Evaluation Service, grounding) are stable even as branding moves.
- Day one I'd verify what's GA vs preview + your quotas.
⭐ You're stronger on AWS than GCP — why trust you to lead on GCP?
- The hard part is AI architecture, evaluation discipline, access control, and leading a scattered team — my depth.
- Clouds are dialects: enterprise IAM, perimeters, customer-managed keys on AWS/Azure map onto IAM, VPC-SC, CMEK directly.
- At Prudential I went zero-to-shipping enterprise search on a managed AI service I hadn't used — SSO + multi-tenant entitlement — and moved a real metric.
- World-class on AI, data, leadership day one; I close the GCP-ops gap in weeks with Google's SEs + your strongest GCP dev.
- You're not hiring a certification — you're hiring someone who ships governed AI systems.
The board could impose a constraint in two months that breaks your design. How do you avoid a house of cards?
- Decisions route through humans; rules externalized + versioned; access control in the data layer; every output cited + audited.
- So "no AI-only settlement figures" or "tighten data residency" are dials I turn, not foundations I re-pour.
- Engage the board during the build with real artifacts — hear the constraint at week four, not week eight.
- Loose coupling between AI, rules, and enforcement, so any one changes without cascading.
You'd inherit 4-5 devs, onshore + offshore, no tech lead. How do you lead?
- Week one: map who knows what + where the onshore/offshore seam causes friction.
- Spine: clear architecture, business rules as versioned artifacts, conventions auto-enforced, eval suite as the shared "done."
- My Claude "staff engineer" + OWASP security-gate skills mean quality doesn't depend on who reviewed the PR — scales across timezones.
- Decompose work into clear, independently testable pieces.
- Document relentlessly with runbooks — knowledge has to survive attrition.
First 90 days — what does delivery look like?
- Days 1-30: architecture + GCP skeleton in a sandbox; prove the hardest risk — split + reassemble a real 1,000pp file with provenance intact.
- Days 31-60: end-to-end thin slice on real de-identified claims (extract → compose → validate → review UI), eval suite live, gold set with your SMEs.
- Days 61-90: harden access control + audit, add validation-agent triage, take it to the governance framework.
- Each milestone has a verifiable check, not a status color.
How do you work with the governance framework, not around it?
- I've built on NIST AI RMF (framework.swapniltamse.com), so I know what a board asks: auditability, human-in-the-loop, evals with a floor, lineage/citations, access control, versioning.
- All of which I'm already building.
- Map our controls to their framework + bring evidence, not promises — "here's the audit table, the eval dashboard, the who-saw-what."
- On a claims system, governance isn't a tax on the build — it is the build.
4 Java / Spring Boot backend
CURE's product dev is on Guidewire (Java); the doc-agent backend is likely Java/Spring Boot on GCP. You have 5 yrs Java + 10 yrs C#/.NET — the enterprise patterns transfer directly.
The Spring Boot service doesn't sit on a request thread doing OCR. It accepts the upload, persists job + document rows, hands heavy work to async workers / Pub/Sub, and exposes status + review. Layered controller → service → repository (maps 1:1 from .NET: Controller, Application layer, EF DbContext). DTOs are Java records — never leak JPA entities over the wire.
POST /api/v1/claims/{claimId}/documents → 202 Accepted + jobId + Location
GET /api/v1/jobs/{jobId} → poll status (QUEUED/SPLITTING/EXTRACTING…)
GET /api/v1/documents/{docId}/extraction → fields/chronology/values (latest version)
GET /api/v1/documents/{docId}/versions → version history
POST /api/v1/documents/{docId}/review → human review (approve/correct/reject)
- Upload returns 202, not the result — a 1,000-page job is minutes long; the client polls or subscribes.
- Idempotency-Key header (or content hash) with a unique constraint → a client retry returns the existing
jobId, not a second 1,000-page run. - Bean Validation on request DTOs; a
@RestControllerAdvicemaps errors to RFC 9457ProblemDetail.
A blocked servlet thread ties up the pool, exhausts DB connections under 50-100 users, and dies on the Cloud Run request timeout. Accept fast, process out-of-band, report status. This is the single most important architectural point in the eval.
| Mechanism | Use when | Notes |
|---|---|---|
| @Async + CompletableFuture | Tight in-process fan-out (embed 20 chunks in parallel) | Not durable — glue, not backbone. Use a bounded pool or virtual threads. |
| Spring Batch | The page-split + per-page extraction | Best fit. Chunked commits, checkpointing, restartability — page 734 fails, restart resumes from the last committed chunk. |
| Pub/Sub (Spring Cloud GCP) | Decoupling agent stages, scaling workers independently | Durable, at-least-once, back-pressure. Each stage = a topic/subscription; survives instance restarts. |
@Bean Step extractPagesStep(JobRepository repo, PlatformTransactionManager tx,
PageItemReader reader, DocumentAiProcessor processor,
ExtractionWriter writer) {
return new StepBuilder("extractPages", repo)
.<PageRef, PageExtraction>chunk(10, tx) // commit every 10 pages
.reader(reader).processor(processor).writer(writer)
.faultTolerant()
.retryLimit(3).retry(TransientAiException.class)
.skipLimit(20).skip(UnprocessablePageException.class)
.build();
}
Idempotent: workers upsert keyed on jobId:pageRange (at-least-once will redeliver). Retryable: Pub/Sub redelivery + dead-letter after N; Spring Retry backoff on AI calls. Restartable: Batch resumes at the last committed chunk.
// Spring AI ChatClient → Gemini, with RAG advisor + structured output
ClaimNarrative generate(ExtractionBundle facts) {
return chat.prompt()
.user(u -> u.text("Draft the claim narrative per business rules:\n{facts}")
.param("facts", facts.asJson()))
.call()
.entity(ClaimNarrative.class); // typed record, not raw text
}
// builder: .defaultAdvisors(QuestionAnswerAdvisor.builder(vectorStore).build())
// Document AI — use batchProcessDocuments for >200 pages (async, GCS in/out)
try (DocumentProcessorServiceClient client = DocumentProcessorServiceClient.create()) {
String name = ProcessorName.of(projectId, location, processorId).toString();
RawDocument raw = RawDocument.newBuilder()
.setContent(ByteString.copyFrom(pdf)).setMimeType("application/pdf").build();
return client.processDocument(
ProcessRequest.newBuilder().setName(name).setRawDocument(raw).build());
}
Auth is Application Default Credentials via Workload Identity — no key files. Spring AI starters: spring-ai-starter-model-vertex-ai-gemini, spring-ai-starter-vector-store-pgvector. Embeddings via VertexAiTextEmbeddingModel → PgVectorStore (HNSW index).
Data layer (Spring)
- JPA/Hibernate for the transactional domain graph (≈ EF Core:
EntityManager=DbContext); JdbcTemplate for bulk field inserts + pgvector queries where you want exact SQL. - Append-only audit (revoke UPDATE/DELETE grants); versioning = new row.
@Version= optimistic lock → 409 on concurrent review edits. - N+1 fix:
JOIN FETCH/@EntityGraph(= EF.Include()). - Flyway migrations incl. pgvector extension + RLS policies.
Access control (defense in depth)
- OAuth2 resource server validating the IdP JWT; pull
tenantId/userIdfrom the principal — never the request body. - Mandatory tenant predicate (Hibernate
@Filter/ base repo) so a dev can't forget it. - Postgres RLS as a second wall;
SET LOCAL app.current_userper transaction. - Vector retrieval carries the tenant filter so RAG never surfaces another user's chunks.
- Secrets via Secret Manager (
${sm://…}), never properties.
Testing
- JUnit 5 + Mockito for units; slices:
@WebMvcTest(controllers/security),@DataJpaTest(repos). - Testcontainers real Postgres + pgvector — H2 lies (no JSONB, vector, or RLS), so it gives false confidence.
- WireMock stubs Document AI / Gemini — assert payloads, simulate 429/500 to prove retry + dead-letter, without billed GCP calls in CI.
Observability & ops
- Actuator health/liveness/readiness; graceful shutdown so in-flight stages drain on scale-down.
- Micrometer → Cloud Monitoring; custom timers: pages/sec, Document AI latency, Gemini token cost, queue depth. Trace ids across the whole job → Cloud Trace.
- Jib for daemonless, distroless images. Cloud Run for API + stage workers (scale-to-zero; mind the request timeout → why work is async); Cloud Run Jobs for Batch drivers; GKE only if you need it.
Guidewire context (know enough, don't overclaim)
- What it is: Guidewire InsuranceSuite — PolicyCenter / ClaimCenter / BillingCenter — the P&C core, built in Java + Gosu, delivered as Guidewire Cloud. The claims documents originate around ClaimCenter.
- Integrate alongside, don't modify internals: consume/produce via the Guidewire Cloud API (REST); an integration gateway / Pub/Sub bridge triggers your ingestion on claim/document events. Mention ACORD (P&C data standards) for domain literacy.
- Stay close to out-of-the-box — heavy customization breaks the ~quarterly cloud upgrade. Your AI service lives outside the core so both upgrade independently.
- Honest framing: "I'm not a Guidewire/Gosu developer. I build the Java/Spring Boot AI service that integrates alongside ClaimCenter through the Cloud API, keeping the core upgrade-safe. I'd lean on your Guidewire team for the platform side and own the AI/data backend."
Design the REST API for the document-processing service.
POST /claims/{id}/documents→ 202 Accepted +jobId+Location: /jobs/{jobId}— never hold an HTTP thread for a minutes-long job.- Then
GET /documents/{id}/extraction,/versions,/narrative;POST /documents/{id}/reviewfor human-in-the-loop. - Requests/responses are
recordDTOs, never JPA entities; Bean Validation; errors as RFC 9457 ProblemDetail. - Uploads carry an Idempotency-Key. Versioned under
/v1.
⭐ How do you handle a 1,000-page job without timing out?
- Document AI caps ~200pp/sync → split into shards in GCS, then drive the pipeline.
- Pub/Sub between stages — durable, at-least-once, independently scalable handoff.
- Spring Batch for page-level extraction — chunked commits + restartability (page 734 fails → resume from the last committed chunk).
@Async/CompletableFuture only for tight in-process fan-out.- Idempotent workers keyed on
jobId + page-range(at-least-once redelivers); dead-letter after N retries.
Spring Data JPA vs JdbcTemplate — and how do you avoid N+1?
- JPA where dirty checking + optimistic locking pay off; JdbcTemplate for bulk field inserts + pgvector search (exact SQL).
- N+1: lazy-loading a collection in a loop → one query per parent.
- Fix:
JOIN FETCHor@EntityGraph(one round trip) +hibernate.jdbc.batch_sizefor bulk writes. - Lazy by default, eager only where needed. Same as EF Core's
.Include()vs lazy tradeoff — familiar.
@Transactional pitfalls.
- Self-invocation — calling a @Transactional method from the same bean bypasses the proxy → no transaction. Fix: injected reference or restructure.
- Private methods — @Transactional does nothing; proxies only wrap public methods.
- Keep them short — never wrap a multi-minute Document AI/Gemini call in a transaction; it pins a connection and blows the pool.
- Orchestration does I/O outside the transaction, opens one only for the persistence step.
Connection pooling under 50-100 concurrent users.
- HikariCP by default; 10-20 connections, not 100 — Postgres does real work per connection, oversized pools cause contention.
- The real fix under load: don't hold connections during slow AI calls — external calls outside transactions so connections cycle fast.
- Tune
maximumPoolSize,connectionTimeout,maxLifetime; watch Hikari's Micrometer metrics for pending threads. - Mind Cloud SQL's own max-connections ceiling across all Cloud Run instances.
How do you call Gemini from Spring AI?
- Add
spring-ai-starter-model-vertex-ai-gemini, configure project/location/model, inject a ChatClient. chat.prompt().system(...).user(...).call().entity(SomeRecord.class)—.entity()gives typed output, no raw-text parsing.- RAG: attach a QuestionAnswerAdvisor over a VectorStore; low temperature for extraction.
- Auth via Workload Identity, no keys. Embeddings via
VertexAiTextEmbeddingModel→ PgVectorStore.
Testing strategy for this service.
- Unit: mock the AI + storage ports — fast, deterministic business logic.
- Slices:
@WebMvcTest(controllers/validation/security);@DataJpaTestagainst real Postgres via Testcontainers (H2 lacks JSONB, pgvector, RLS → false confidence). - WireMock stubs Document AI + Gemini — assert payloads, simulate 429/500 to prove retry + dead-letter, no billed GCP calls.
- A few
@SpringBootTestpaths for full orchestration.
⭐ Your Java is less fresh than your C# — how does that play here?
- 10 yrs C#/.NET, 5 yrs Java. DI is DI; Spring's controller-service-repository is the architecture I've led in .NET.
- JPA/Hibernate ≈ EF Core:
EntityManager=DbContext, repositories=DbSet, same change tracking + lazy loading,JOIN FETCH=.Include(). - Not learning software design — re-seating syntax + libraries. Days, not months.
- Player-coach: pair with strong Java devs on idiom while I own architecture, AI/data design, delivery; validate current Spring APIs against docs.
How do you enforce access control in the service layer?
- Stateless OAuth2 resource server validating the IdP JWT; pull
tenantId/userIdfrom the principal, never the request body. - Mandatory tenant predicate on every query (Hibernate filter / base repo).
- Postgres RLS as a second wall;
@PreAuthorizegates sensitive ops by scope. - Critically, vector retrieval carries the tenant filter so RAG never surfaces another user's chunks.
- Append-only audit on every access. Same principle as .NET — mechanics differ, model identical.
Where does the validation agent fit, and how do you audit everything?
- Validation agent runs after narrative generation, checking it against extracted facts + business rules.
- Its verdict is a versioned record — generated output + independent check side by side.
- Append-only audit: uploads, every extraction version, every review decision, every verdict — actor + timestamp + trace id, insert-only table (revoked update/delete grants).
- Plus
@Versionoptimistic locking. One trace id across the chain reconstructs any claim in Cloud Trace.