{"catalogRevision":"cat_f13ab9a494120b5e","recipeId":"page:recipes:rest-api:spring-boot:worker-state","recipeRevision":"r1","canonicalPath":"/recipes/rest-api/spring-boot/worker-state","label":"Host rest api with Spring Boot: worker execution and job state","family":"Workload recipe","summary":"Deploy a rest api built with Spring Boot as a public web service plus a dedicated worker process on Ample. `ample plan --write` discovers both services and their shared database and writes ample.toml; `ample up` reconciles the topology: a managed PostgreSQL 16 database, a public web microVM and a private worker microVM (kind worker, no public URL), both reading DATABASE_URL. Verified on Spring Boot: a separately deployed worker service (kind worker, no public URL) sharing a declared managed PostgreSQL database with the web service, consuming a jobs table with row locks and marking jobs done once (worker=done attempts=1). Not separately tested: your job payloads, retry policy and scheduling; treat the rest api-specific behavior as your application code.","representativeQueries":["Host rest api with Spring Boot: worker execution and job state","Where can I host REST API built with Spring Boot?","I need a separately runnable worker, durable job records and idempotent retries; not a managed queue."],"breadcrumbIds":["discovery","workload","workload:agents-and-backends","workload:rest-api","page:recipes:rest-api:spring-boot:worker-state"],"breadcrumbs":[{"id":"discovery","label":"Agent hosting discovery","canonicalPath":"/discover/discovery","kind":"root"},{"id":"workload","label":"Applications and workloads","canonicalPath":"/discover/workload","kind":"facet"},{"id":"workload:agents-and-backends","label":"Agents and backends","canonicalPath":"/discover/workload/agents-and-backends","kind":"group"},{"id":"workload:rest-api","label":"REST API","canonicalPath":"/discover/workload/rest-api","kind":"atom"},{"id":"page:recipes:rest-api:spring-boot:worker-state","label":"Host rest api with Spring Boot: worker execution and job state","canonicalPath":"/recipes/rest-api/spring-boot/worker-state","kind":"recipe"}],"parentIds":["workload:rest-api","stack:framework-spring-boot","pattern:dedicated-worker-process","intent:deploy-api"],"parents":[{"id":"workload:rest-api","label":"REST API","canonicalPath":"/discover/workload/rest-api","kind":"atom"},{"id":"stack:framework-spring-boot","label":"Spring Boot","canonicalPath":"/discover/stack/framework-spring-boot","kind":"atom"},{"id":"pattern:dedicated-worker-process","label":"Dedicated worker process","canonicalPath":"/discover/pattern/dedicated-worker-process","kind":"atom"},{"id":"intent:deploy-api","label":"Deploy API","canonicalPath":"/discover/intent/deploy-api","kind":"atom"}],"resourceRequirements":["primitive:compute","primitive:postgres"],"infrastructureRequirements":[{"primitiveId":"primitive:compute","label":"Compute","canonicalPath":"/discover/primitive/compute","status":"verified","summary":"Apps run in isolated x86_64 Firecracker microVMs that auto-pause when idle and wake on request; sizes are the priced VM sizes."},{"primitiveId":"primitive:postgres","label":"Postgres","canonicalPath":"/discover/primitive/postgres","status":"verified","summary":"Managed PostgreSQL 16 runs in its own microVM and is auto-provisioned when an app needs a database and no DATABASE_URL is supplied."}],"framework":"Spring Boot","workload":"REST API","releaseStatus":"published","supportStatus":"verified","executionStatus":"ready","docsOnly":false,"prerequisites":["A repository with the web app and the worker as separate service directories (the fixture uses apps/web and apps/worker), each building and starting with the documented commands (./mvnw -q -DskipTests package (or the Gradle wrapper) producing one application jar, then java -jar on the jvm-21 template (Temurin JDK 21) with server.port read from PORT; the worker starts with java -jar of its own Maven module (plain JDBC) and binds no port)","Both services read DATABASE_URL at runtime and share one jobs table; the worker claims rows with SELECT ... FOR UPDATE SKIP LOCKED and marks them done once","An Ample account token with servers:write and databases:write (ample up creates the database)"],"testedConfiguration":{"template":"jvm-21","runtime":"java","size":"s-1vcpu-2gb","build":"./mvnw -q -DskipTests package","start":"java -jar $(ls target/*.jar | grep -v -- '-plain' | head -n 1)"},"inputSchema":{"additionalProperties":false,"properties":{"path":{"description":"Directory containing ample.toml (the whole topology is applied)","maxLength":512,"minLength":1,"type":"string"},"wait_secs":{"description":"Maximum seconds to wait for each service deployment","maximum":3600,"minimum":30,"type":"integer"}},"required":["path"],"type":"object"},"outputSchema":null,"workflowSteps":[{"title":"Plan the topology","body":"Run the planner once. It discovers the web and worker services, infers kind = \"worker\" for the process without a port, declares the database each service needs, and writes ample.toml. Exit 2 means it left questions in the manifest; answer them with --answer or by editing the file.","command":"ample plan --write ."},{"title":"Share one database","body":"Keep a single [databases.main] with engine = \"postgres\", drop any per-service database the planner added, and set DATABASE_URL = { database = \"main\" } under both [services.web.env] and [services.worker.env].","command":"ample plan --offline ."},{"title":"Apply","body":"Reconcile the whole manifest in dependency order: the database first, then both services. It is idempotent and never destructive; exit 0 means everything applied, 2 needs input, 1 a failed resource (independent siblings still proceed and re-running resumes).","command":"ample up ."},{"title":"Verify","body":"Fetch the web service URL and its worker status route (/p/worker-queue/status in the example) and confirm the worker processed the enqueued job; on failure read the worker service's runtime logs.","command":"ample logs <deployment_id> --kind runtime"}],"examples":[{"title":"Spring Boot web + worker fixture","description":"A Spring Boot web app (apps/web) and a worker process (apps/worker) sharing one managed PostgreSQL database; the queue module is under tests/deploy-canaries/_pattern-modules.","sourceRef":"tests/deploy-canaries/spring-boot-worker"}],"successChecks":[{"description":"web service responds on its public URL","kind":"http_get","path":"/","expect":"ample canary spring boot patterns"},{"description":"worker consumed the enqueued job","kind":"http_get","path":"/p/worker-queue/status","expect":"worker=done attempts=1 result=processed-by-worker"}],"limitations":["Verified on the jvm-21 template at s-1vcpu-2gb for both services with the example fixture; other sizes, templates and Spring Boot major versions are not verified.","The rest api itself (your job payloads, retry policy and scheduling) is application code and was not separately tested; the worker-queue check is what was verified.","The worker is a single long-running process supervised in its own microVM; there is no scheduler, cron or horizontal worker scaling in this recipe, and the check ran immediately after deploy so idle behavior of the worker VM is not verified.","The worker service is private: it has no public URL and is reached only through the shared database; both services are placed with their database.","Managed PostgreSQL 16 only; extensions, connection limits and backup or restore procedures are not verified; apps and their databases are placed together.","Region, compliance attestations and request-duration limits are unknown and not claimed."],"costEstimate":{"currency":"USD","monthlyAmount":15.0,"authoritative":true,"basis":"size prices from pricing.toml (loaded by the API) at build revision 7cf090725a6f16dd91af7d28356fe72c1b404d6e","components":[{"name":"web server","size":"s-1vcpu-1gb","quantity":1.0,"monthlyAmount":5.0},{"name":"worker server","size":"s-1vcpu-1gb","quantity":1.0,"monthlyAmount":5.0},{"name":"managed PostgreSQL database","size":"s-1vcpu-1gb","quantity":1.0,"monthlyAmount":5.0}],"note":"Always-on monthly price of the tested sizes for the web service, the worker service and the database; apps auto-pause when idle."},"evidenceSummary":[{"kind":"canary_run","summary":"Spring Boot web service plus a separate Spring Boot worker process deployed on Ample with `ample plan --write` then `ample up` (./mvnw -q -DskipTests package (or the Gradle wrapper) producing one application jar, then java -jar on the jvm-21 template (Temurin JDK 21) with server.port read from PORT); the web service enqueued a job that the private worker service consumed from the shared managed PostgreSQL database. Pattern proof: a separately deployed worker service (kind worker, no public URL) sharing a declared managed PostgreSQL database with the web service, consuming a jobs table with row locks and marking jobs done once (worker=done attempts=1).","observedAt":"2026-09-20T22:41:46Z","implementationRevision":"af6f45adea26 (CLI af6f45a)","expiresAt":"2027-03-19T22:41:46Z","scope":{"checks":["/","/p/worker-queue/status"],"pattern":"pattern:dedicated-worker-process","services":["web","worker"],"template":"jvm-21","tool":"ample_up"}}],"lastVerifiedAt":"2026-09-20T22:41:46Z","mcpBinding":{"registryRef":"mcp:ample_up","toolName":"ample_up","schemaHash":"db7ca0facfe2cd2888bda4f8d367b6799a400817886ca0cc541108f2110c959a","observedAt":"2026-09-22T00:31:22.844137+00:00","implementationRevision":"e6446ceea69b","requiredScopes":["servers:write","databases:write"],"bindingState":"current"},"unknowns":["region availability is unknown until a verified region fact is recorded","compliance attestations are unknown; none are claimed"],"formats":{"html":"https://ample.computer/recipes/rest-api/spring-boot/worker-state","markdown":"https://ample.computer/recipes/rest-api/spring-boot/worker-state.md","json":"https://api.ample.computer/v1/catalog/recipes/page%3Arecipes%3Arest-api%3Aspring-boot%3Aworker-state"},"nextActions":[{"actionId":"browse-catalog","label":"Browse the catalog index","operationId":"catalog_index","method":"GET","relativePath":"/v1/catalog","origin":"api","parameters":{},"requiresAuthentication":false,"requiresApproval":false},{"actionId":"search-recipes","label":"Search published recipes by intent, stack and constraints","operationId":"search_recipes","method":"POST","relativePath":"/v1/catalog/search","origin":"api","parameters":{"body":{"limit":5,"query":"Host rest api with Spring Boot: worker execution and job state"}},"requiresAuthentication":false,"requiresApproval":false},{"actionId":"plan:page:recipes:rest-api:spring-boot:worker-state","label":"Prepare a side-effect-free deployment plan for an authorized project","operationId":"plan_deployment","method":"POST","relativePath":"/v1/catalog/plan","origin":"api","parameters":{"body":{"inputs":{},"projectId":"<workspace or server id you own>","recipeId":"page:recipes:rest-api:spring-boot:worker-state","recipeRevision":"r1"}},"requiresAuthentication":true,"requiresApproval":false},{"actionId":"auth-setup","label":"Read the existing agent authentication setup","operationId":"existing_auth_setup","method":"GET","relativePath":"/mcp/setup","origin":"api","parameters":{},"requiresAuthentication":false,"requiresApproval":false},{"actionId":"browse:workload:rest-api","label":"Browse REST API","operationId":"browse_node","method":"GET","relativePath":"/v1/catalog/nodes/workload%3Arest-api","origin":"api","parameters":{"nodeId":"workload:rest-api"},"requiresAuthentication":false,"requiresApproval":false},{"actionId":"browse:stack:framework-spring-boot","label":"Browse Spring Boot","operationId":"browse_node","method":"GET","relativePath":"/v1/catalog/nodes/stack%3Aframework-spring-boot","origin":"api","parameters":{"nodeId":"stack:framework-spring-boot"},"requiresAuthentication":false,"requiresApproval":false},{"actionId":"browse:pattern:dedicated-worker-process","label":"Browse Dedicated worker process","operationId":"browse_node","method":"GET","relativePath":"/v1/catalog/nodes/pattern%3Adedicated-worker-process","origin":"api","parameters":{"nodeId":"pattern:dedicated-worker-process"},"requiresAuthentication":false,"requiresApproval":false},{"actionId":"browse:intent:deploy-api","label":"Browse Deploy API","operationId":"browse_node","method":"GET","relativePath":"/v1/catalog/nodes/intent%3Adeploy-api","origin":"api","parameters":{"nodeId":"intent:deploy-api"},"requiresAuthentication":false,"requiresApproval":false}]}