Status: technical blueprint / prototype — not a compliance product.
This repository is a small, readable Spring Boot codebase that works out the service and concurrency shape for a compliance-data API. It contains no compliance rules, no regulatory logic, and no legal validation of any kind, and it has never been deployed to a production environment. Nothing here constitutes legal or regulatory advice. The endpoints named
complyare orchestration scaffolding — see What this prototype demonstrates for a precise account of what is and isn't implemented.
Regulatory and compliance workflows push entity data (companies, filings, registrations) back and forth between an internal system of record and one or more third-party APIs — registries, screening vendors, government endpoints. That workload has an awkward shape:
- Submissions arrive in batches, not one at a time. An onboarding run or a periodic refresh means hundreds of records at once.
- The slow part is someone else's network. Latency is dominated by external calls the service does not control and cannot speed up.
- Results must be reconciled, because a batch partially succeeds: some records land, some fail, and the caller needs an accounting of which.
Handled naively — a blocking call per record on the request thread — the servlet pool spends its life parked on external I/O, and throughput collapses well before CPU or database capacity is the constraint.
This repository is a blueprint for the API layer that sits in front of that problem: how the controllers, services, and thread pools should be arranged so the request thread is released while external work is in flight.
The planned dataset-to-law model keeps company-owned data separate from the
versioned compliance rule catalog and uses RuleEvaluation as the traceable
evidence object. See the Mermaid domain diagrams.
What is implemented and verified (exercised by the test suite and by live
curl against a running instance):
- A complete CRUD resource (
/companies) in both blocking andCompletableFuture-returning variants, sharing one service and persistence layer. - Real concurrent outbound HTTP fan-out via
java.net.http.HttpClient(HTTP/2, 10 s connect timeout), aggregated withCompletableFuture.allOf. - A custom
@Asyncexecutor (10 core / 20 max threads, 100-item queue) and verified evidence of which endpoints actually use it — see Concurrency design. - Layered separation: controller → service interface → implementation → mapper → repository → JPA entity → DTO record, with constructor injection throughout.
- Error mapping:
ResourceNotFoundException→404;.exceptionally(...)on async chains →500. - OpenAPI 3 generated from the controllers, served at
/swagger-ui/index.html(with/swagger-ui.htmlredirect).
What the architecture demonstrates rather than delivers: the /api/comply
endpoints show where compliance orchestration would attach — a request enters,
fans out to an external endpoint supplied by the caller, and a reconciliation
step summarises the responses. The reconciliation implementation counts
non-empty response strings. It does not parse, validate, or interpret them.
What is deliberately absent: compliance rules, a rule engine, external registry integrations, authentication, authorization, request validation, transaction boundaries, retries, rate limiting, and persistence beyond an in-memory default. See Production considerations.
flowchart TB
client(["HTTP client"])
subgraph web ["Spring MVC — @RestController"]
home["HomeController<br/>GET / → redirect to Swagger UI"]
companyC["CompanyController<br/>/companies"]
complyC["ComplyController<br/>/api/comply"]
end
subgraph svc ["Service layer — interface + impl"]
companyS["CompanyServiceImpl<br/>sync + @Async CRUD"]
complyS["ComplyApiServiceImpl<br/>orchestration"]
httpS["AsyncHttpServiceImpl<br/>java.net.http.HttpClient (HTTP/2)"]
end
subgraph data ["Persistence"]
mapper["CompanyMapper<br/>entity ⇄ DTO"]
repo["CompanyRepository<br/>Spring Data JPA"]
db[("H2 in-memory — default<br/>MySQL — 'mysql' profile")]
end
pool["AsyncConfig · taskExecutor<br/>10 core / 20 max / queue 100"]
docs["springdoc-openapi<br/>/swagger-ui.html · /v3/api-docs"]
ext(["External HTTP endpoint<br/>URL supplied per request"])
client --> home
client --> companyC
client --> complyC
client -.->|"reads"| docs
docs -.->|"introspects"| companyC
docs -.->|"introspects"| complyC
companyC --> companyS
complyC --> complyS
complyS --> companyS
complyS --> httpS
httpS -->|"concurrent POSTs"| ext
companyS --> mapper
companyS --> repo
repo --> db
pool -.->|"executes @Async methods"| companyS
The diagram shows every component that participates in a request. One class in
the repository does not appear because nothing calls it:
util/ConcurrentFileWriter is a standalone ReentrantLock-guarded file-append
helper, not wired into any request path.
| Capability | Where | Notes |
|---|---|---|
Sync CRUD for Company |
CompanyController, CompanyServiceImpl |
Create / read / update / delete |
| Async CRUD variants | same | Return CompletableFuture<ResponseEntity<T>> |
| Batch create | POST /companies/batch/async |
Per-item futures joined with allOf |
| Concurrent outbound HTTP | AsyncHttpServiceImpl |
HTTP/2, sendAsync, allOf aggregation |
| Response reconciliation | ComplyApiServiceImpl |
Counts non-null, non-empty responses |
| Custom thread pool | AsyncConfig |
10/20/100, Async- thread prefix |
| 404 / 500 mapping | ResourceNotFoundException, .exceptionally |
|
| OpenAPI 3 + Swagger UI | OpenApiConfig, springdoc |
Generated from controllers |
| Root redirect | HomeController |
/ → /swagger-ui/index.html |
| H2 default, MySQL profile | application*.properties |
DB credentials from env vars only |
| Unit + integration tests | src/test/java/… |
17 tests, Mockito + MockMvc |
| Container build | Dockerfile |
Multi-stage, non-root, slim JRE |
| CI | .github/workflows/ci.yml |
mvn -B verify on push / PR |
| Layer | Technology |
|---|---|
| Runtime | Java 17+, Spring Boot 3.5.6 |
| Web | Spring Web MVC, Jackson |
| Persistence | Spring Data JPA, Hibernate 6.6, H2 (default) / MySQL |
| Async | CompletableFuture, Spring @Async, ThreadPoolTaskExecutor |
| Docs | springdoc-openapi 2.6.0 (OpenAPI 3 + Swagger UI) |
| Build | Maven |
| Tests | JUnit 5, Mockito, Spring Boot Test, MockMvc, AssertJ |
JDK 17+ and Maven 3.6+. This repository has no mvnw wrapper, so Maven
must be installed.
On macOS, via Homebrew:
# Install Homebrew if you don't have it
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# Homebrew then prints the two lines needed to add it to your PATH, e.g.
# echo 'eval "$(/usr/local/bin/brew shellenv)"' >> ~/.zprofile
# eval "$(/usr/local/bin/brew shellenv)"
brew install openjdk mavenmvn clean verifymvn spring-boot:runor run the packaged jar:
mvn clean package
java -jar target/demo-0.0.1-SNAPSHOT.jarThe app starts on http://localhost:8080 backed by an in-memory H2 database. No configuration, credentials, or external services are required. Data is discarded when the process exits.
- Swagger UI → http://localhost:8080/swagger-ui.html
- OpenAPI 3 JSON → http://localhost:8080/v3/api-docs
Credentials are read from environment variables; nothing sensitive is committed.
export DB_URL=jdbc:mysql://localhost:3306/ems
export DB_USERNAME=your_user
export DB_PASSWORD=your_password
mvn spring-boot:run -Dspring-boot.run.profiles=mysqldocker build -t comply-api-blueprint .
docker run --rm -p 8080:8080 comply-api-blueprintThe multi-stage Dockerfile builds with Maven + JDK 17 and ships an
eclipse-temurin:17-jre-alpine runtime running as a non-root user.
This service is not deployed anywhere, and no hosted instance exists.
A render.yaml blueprint is included and the application is deployment-shaped —
it reads $PORT (server.port=${PORT:8080}), needs no secrets under its
default H2 profile, and builds from the committed Dockerfile. Standing up an
actual instance requires a Render account and provisioning outside this
repository, so it has not been done and is not claimed. Anyone wanting a live
instance can point Render's New + → Blueprint flow at a fork; note that the
default profile's H2 database is in-memory, so a hosted instance would lose all
data on restart, and a persistent deployment would need the MySQL profile plus a
provisioned database.
Base URL http://localhost:8080. All request and response bodies are JSON.
Every example below was executed against a running instance; the status codes
shown are the observed responses.
| Method | Path | Response | Description |
|---|---|---|---|
| GET | /companies |
200 |
List all companies |
| GET | /companies/{id} |
200 / 404 |
Get by id |
| POST | /companies |
201 |
Create (blocking) |
| PUT | /companies/{id} |
200 |
Update (blocking) |
| DELETE | /companies/{id} |
204 |
Delete |
| POST | /companies/async |
201 |
Create on the async pool |
| POST | /companies/batch/async |
201 |
Batch create, futures joined by allOf |
| PUT | /companies/{id}/async |
200 |
Update, non-blocking dispatch |
| Method | Path | Response | Description |
|---|---|---|---|
| POST | /api/comply/process |
201 |
Persist one record on the async pool |
| POST | /api/comply/process/batch |
201 |
Persist a batch |
| POST | /api/comply/external-api/concurrent |
200 |
Fan out concurrent POSTs to ?apiEndpoint= |
| POST | /api/comply/reconcile |
200 |
Summarise a list of response strings |
# Create — 201
curl -s -X POST http://localhost:8080/companies \
-H 'Content-Type: application/json' \
-d '{"id":null,"name":"Acme","email":"acme@example.com"}'
# {"id":1,"name":"Acme","email":"acme@example.com"}
# List — 200
curl -s http://localhost:8080/companies
# [{"id":1,"name":"Acme","email":"acme@example.com"}]
# Unknown id — 404
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8080/companies/999999
# Batch create — 201
curl -s -X POST http://localhost:8080/companies/batch/async \
-H 'Content-Type: application/json' \
-d '[{"id":null,"name":"B1","email":"b1@x.com"},{"id":null,"name":"B2","email":"b2@x.com"}]'
# [{"id":2,"name":"B1",...},{"id":3,"name":"B2",...}]
# Reconcile a set of responses — 200
curl -s -X POST http://localhost:8080/api/comply/reconcile \
-H 'Content-Type: application/json' \
-d '["{\"status\":\"ok\"}","{\"status\":\"ok\"}"]'
# Reconciliation complete: 2/2 responses processedBreaking change: the company resource was previously mounted at
/companys. It is now/companies. No compatibility alias is provided — the old path returns404. The JPA table is still namedcompanys; that is an internal schema detail and was left unchanged.
The async layer is the part of this blueprint worth reading closely, including where it currently falls short.
Thread hand-off. Async service methods are annotated @Async and return
CompletableFuture<T>, scheduled onto the taskExecutor bean in AsyncConfig
(10 core / 20 max threads, 100-item queue, Async- name prefix). Controllers
return CompletableFuture<ResponseEntity<T>>, so Spring MVC releases the
servlet thread and completes the response when the future resolves.
Fan-out and aggregation. AsyncHttpServiceImpl builds one HttpRequest
per entry, dispatches them all with sendAsync, and joins them with
CompletableFuture.allOf(...) before mapping each result — the requests are
genuinely in flight simultaneously.
Error propagation. Async chains terminate in .exceptionally(...) at the
controller, converting a failed stage into a 500 rather than a leaked
exception. ResourceNotFoundException carries @ResponseStatus(NOT_FOUND).
Verified execution threads. Running the app with
--logging.level.org.hibernate.SQL=DEBUG and watching which thread issues each
insert shows exactly where work lands:
| Endpoint | Thread observed | Meaning |
|---|---|---|
POST /companies |
nio-8080-exec-6 |
Blocking, on the request thread — as intended |
POST /companies/async |
Async-1 |
Dispatched to taskExecutor — hand-off works |
POST /api/comply/process |
Async-3 |
Cross-bean call, proxy applies |
POST /companies/batch/async |
nio-8080-exec-2 (both inserts) |
Runs on the request thread |
POST /api/comply/reconcile |
ForkJoinPool.commonPool worker |
Not the configured pool |
Two of those deserve comment, because they are the classic @Async traps:
- The batch endpoint does not currently run in parallel.
createCompaniesAsyncis not itself@Asyncand callsthis.createCompanyAsync(...). Self-invocation bypasses the Spring AOP proxy, so the@Asyncannotation has no effect and every item is persisted sequentially on the caller's thread. TheallOfaggregation is structurally correct and would parallelise as soon as the calls go through the proxy; the wiring is what's missing. reconcileApiResponsesusesCompletableFuture.supplyAsync(...)with no executor argument, so it runs on the JVM's commonForkJoinPoolrather than the poolAsyncConfigdefines.
Both are left as-is here rather than quietly fixed, so that the documented behaviour matches the committed code.
Accurate scope, stated plainly. This codebase would need all of the following before it could carry real traffic:
- No compliance logic. No rules, no regulatory validation, no registry
integrations. The
complynaming describes intent, not capability. - The batch path is not actually parallel — see above.
- No authentication or authorization. Every endpoint is fully open.
- No request validation.
spring-boot-starter-validationis on the classpath but no constraints are declared and no handler uses@Valid; malformed or missing fields are not rejected. - No transaction boundaries. Nothing is annotated
@Transactional, so a partially-failed batch leaves partial writes. external-api/concurrentposts to a caller-supplied URL with no allow-list, timeout budget beyond the connect timeout, retry, or circuit breaker — server-side request forgery is unaddressed.- Persistence defaults to in-memory H2 with
ddl-auto=update; there are no schema migrations (Flyway/Liquibase) and no seeded data. - No observability. No metrics, tracing, health endpoints beyond the container default, or structured logging.
- Tests cover the layers, not the concurrency. 17 tests verify CRUD behaviour, error mapping, and service orchestration. There is no load test and no test asserting which thread executes what; the table above was produced by manual inspection of a running instance.
ConcurrentFileWriteris dead code — thread-safe and functional, but nothing calls it.
src/main/java/io/github/mjh5153/complyapi/
├── DemoApplication.java # Spring Boot entry point
├── controller/
│ ├── CompanyController.java # /companies (sync + async CRUD)
│ ├── ComplyController.java # /api/comply (orchestration endpoints)
│ └── HomeController.java # / → Swagger UI
├── service/
│ ├── CompanyService.java
│ ├── ComplyApiService.java
│ ├── AsyncHttpService.java
│ └── impl/ # implementations
├── repository/CompanyRepository.java
├── mapper/CompanyMapper.java
├── entity/Company.java # JPA entity (table: companys)
├── dto/CompanyDTO.java # immutable record DTO
├── exception/ResourceNotFoundException.java
├── config/
│ ├── AsyncConfig.java # taskExecutor thread pool
│ └── OpenApiConfig.java # OpenAPI metadata
└── util/ConcurrentFileWriter.java # standalone, not wired in
src/main/resources/
├── application.properties # H2 defaults
└── application-mysql.properties # opt-in MySQL profile
src/test/java/… # Mockito unit + MockMvc integration tests
MIT