Optimisation Service Documentation
Three ways to access the service, depending on how much you want it to do for you. All three ultimately talk to the same REST API — you choose the level of abstraction that fits your situation.
REST API
Submit models in MPS or LP format over HTTP. The foundation that everything else is built on.
Client Library
A lightweight Java client with a fluent modelling API and a simple HTTP facade. One Maven dependency, zero transitive dependencies.
ojAlgo Integration
Plug the service into ExpressionsBasedModel. Your existing models solve remotely without code changes.
Running the service
The Optimisation Service is a container image that you run on your own infrastructure. There is no hosted instance and no account to create — Optimatika never sees your models.
Obtaining the image
The image is public. There is no registry account to create, no credentials to configure and no pull secret to manage in your cluster:
docker pull ghcr.io/optimatika/optimisation-service
Run without a licence key and the server starts in restricted mode — ojAlgo’s own solvers, on a single core. That is a working server, free forever. Licence keys, obtained with an Optimatika Subscription, unlock the full solver suite and the licensed number of vCPUs.
Licence keys
A subscription issues one key per step of the ladder you have bought. A Standard subscription issues two, Extra three, Unlimited four. Each key on its own entitles the deployment to that step in full — the Extra key alone is worth eight vCPUs, whether or not the keys below it are present.
Supply them all the same way, comma- or whitespace-separated:
OPTIMATIKA_LICENCE_KEY=key-one,key-two,key-three
Or, for a mounted Kubernetes secret, one key per line in a file:
OPTIMATIKA_LICENCE_KEY_FILE=/etc/optimatika/licence-keys
Supply every key you hold, even though any one of them would do. It costs nothing and it is what makes a plan change safe:
- Moving up issues one additional key, a new one. The keys you already hold are untouched and the deployment keeps running, but it keeps running at the capacity you had — the new step applies once you add the new key and restart, at a time you choose. Collect it from the customer portal after any upgrade.
- Moving down withdraws the top key and leaves the rest alone. If you configured the whole set, the deployment settles onto the step you moved to. If you configured only the top key, there is nothing left to fall back to and the server drops to the free tier — one core, ojAlgo’s own solvers.
- Switching between monthly and annual billing changes nothing at all. The same keys carry over.
Change your plan; do not cancel and subscribe again. A plan change keeps every key you hold. Cancelling and starting a new subscription reissues all of them, and every deployment you run then has to be reconfigured. If you cancel and resubscribe later, expect an entirely new set.
Keys do not expire while the subscription runs, and a key withdrawn by a downgrade comes back unchanged if you move up again.
Capacity is decided once, when the server starts, because the thread pools are sized from it. A change to your keys therefore takes effect at the next restart; the solver suite follows within twelve hours either way.
GET /optimisation/v1/environment reports how many keys validated and the capacity they resolved to. That is the check to run after any plan change.
Kubernetes
Kustomize manifests ship with the product — a base plus one overlay per cloud — providing a Deployment, a Service and a HorizontalPodAutoscaler:
kubectl apply -k k8s/overlays/gcp
The container listens on port 8080, configurable with the PORT environment variable. Both probes should target /health:
readinessProbe:
httpGet: { path: /health, port: 8080 }
periodSeconds: 10
livenessProbe:
httpGet: { path: /health, port: 8080 }
periodSeconds: 30
Sizing
The shipped manifests request 500m CPU and 512Mi memory per pod. Solvers are CPU-bound and several are multi-threaded, so give a pod whole cores rather than fractions if you are solving anything substantial — 2–4 vCPU is a reasonable starting point for production models. Memory scales with model size rather than with request volume.
GET /optimisation/v1/environment reports what the running server actually sees, which is the quickest way to confirm a pod got the resources you intended.
Security
The API is unauthenticated. There is no API key, no token and no built-in authorisation of any kind. This is deliberate: the service is designed to run inside your own network, where your existing controls apply, rather than to re-implement an identity system you already have.
The consequence is that reachability is your access control, so the deployment must not be publicly reachable:
- Keep the Service internal —
ClusterIP, or an internal load balancer. Note that the shipped base manifest usestype: LoadBalancer, which on a public cluster will allocate an external address; override this unless you intend it. - If it must be reachable from outside the cluster, put an authenticating proxy or API gateway in front of it and terminate TLS there. The service itself speaks plain HTTP.
- Restrict traffic to the pods that need it with a NetworkPolicy.
There is no per-caller rate limiting and no maximum model size. The only backpressure is the work queue, which holds 128 pending problems and returns 429 once full — that protects the server from overload, but it does not stop one caller from filling the queue. Treat access as you would access to a compute cluster.
Submitted models and results are held in memory only, in caches that expire one hour after last access. Nothing is written to disk, and nothing is transmitted off the pod.
Status codes
| Code | Meaning |
|---|---|
200 | Success. For poll-result, check the returned status — 200 means the request succeeded, not that the solve has finished. |
400 | Malformed request — an unparseable model, or an unknown format or sense in the path. |
404 | Unknown endpoint, or a result key that has expired or never existed. |
405 | Wrong HTTP method for that path. |
429 | The work queue is full. Retry with backoff. |
500 | The solve or translation failed unexpectedly. |
Error responses carry the status code and an empty body; the detail is written to the container log. There is no server-side timeout on a solve, so set a client-side one — a large mixed-integer model can run for a long time by design.
REST API
The service exposes a small HTTP API. You submit a serialised model, receive a queue key, and poll until the result is ready. The protocol is intentionally simple — any language with an HTTP client can use it.
Base URL
The base URL is the address of your deployed service instance, e.g. https://your-service-host. All endpoints are relative to this.
Endpoints
| Method | Path | Description |
|---|---|---|
POST |
/optimisation/v1/put-on-queue/{format}/{sense} |
Submit a model for solving |
GET |
/optimisation/v1/poll-result/{key} |
Check status and retrieve the solution |
POST |
/optimisation/v1/translate/{from}/{to} |
Convert a model between formats |
GET |
/optimisation/v1/test |
Check service availability and list loaded solvers |
GET |
/optimisation/v1/environment |
Runtime environment details (JVM, memory, threads) |
GET |
/optimisation/v1/version |
Which build is running — confirms a deployment rolled out |
curl -s https://your-service-host/optimisation/v1/version
{"module":"gcp","built":"2026-08-01T05:42:25Z",
"commit":"74fd4f6","branch":"develop","dirty":"false"}
Submit a model
POST /optimisation/v1/put-on-queue/{format}/{sense}
Path parameters:
| Parameter | Values | Description |
|---|---|---|
format |
MPS, LP |
The format of the request body. MPS is the industry standard; LP is the CPLEX LP format. Both are plain text and are written by virtually every optimisation tool. |
sense |
MIN, MAX |
Whether to minimise or maximise the objective. |
The request body is the raw model bytes. The response is JSON:
{
"key": "PmkvX3SNQ0gjRtCD",
"status": "PENDING"
}
The key is the queue identifier you use to poll for the result.
Poll for the result
GET /optimisation/v1/poll-result/{key}
While the solver is working, the response has "status": "PENDING". When done:
{
"key": "PmkvX3SNQ0gjRtCD",
"status": "DONE",
"result": "OPTIMAL 13.0 @ { 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 }"
}
The result string has three parts: the solver state (OPTIMAL, FEASIBLE, INFEASIBLE, etc.), the objective value, and the solution vector.
Translate between formats
POST /optimisation/v1/translate/{from}/{to}
Converts a model between EBM, MPS, and LP formats. The request body is the model in the input format; the response body is the model in the output format.
Example with curl
# Check that the service is running curl -s https://your-service-host/optimisation/v1/test # Submit an MPS model for minimisation KEY=$(curl -s -X POST \ -H "Content-Type: application/octet-stream" \ --data-binary @model.mps \ https://your-service-host/optimisation/v1/put-on-queue/MPS/MIN \ | jq -r '.key') # Poll for the result curl -s https://your-service-host/optimisation/v1/poll-result/$KEY
Client Library
The client library is a single Maven dependency with zero transitive dependencies. It provides two things: a thin HTTP client (OptClientV1) and a lightweight modelling API (OptModel). Use either or both.
Add the dependency
<dependency>
<groupId>se.optimatika</groupId>
<artifactId>optimisation-service-client</artifactId>
<version>0.1.1</version>
</dependency>
Quick start
Build a model, solve it, read the result — all in a few lines:
// Connect to the service
OptClientV1 client = OptClientV1.newInstance("https://your-service-host");
// Build a model
OptModel model = client.newModel();
OptVariable x = model.newRealVariable("x").lower(0);
OptVariable y = model.newRealVariable("y").lower(0);
model.newConstraint("budget").set(x, 1).set(y, 1).upper(10);
model.objective().set(x, 3).set(y, 5);
// Solve remotely (returns a Future)
OptResult result = model.maximise().get();
System.out.println("Optimal: " + result.isOptimal());
System.out.println("Value: " + result.getValue());
System.out.println("x = " + x.doubleValue());
System.out.println("y = " + y.doubleValue());
The two layers
The client is split into two independent layers that you can use separately:
OptClientV1 is a thin HTTP facade. It knows how to submit bytes to the server and parse the JSON response, but knows nothing about what a model looks like. You can use it standalone to submit MPS or LP files, or any other format the server supports.
OptModel is a model builder. It provides the fluent API for variables, constraints, and objectives, handles serialisation, and manages the polling loop and result mapping. It delegates all HTTP communication to an OptClientV1 instance.
This separation means you can use the HTTP client directly without the model builder, submit MPS files from other tools, or replace the model builder entirely.
HTTP client directly
If you already have models in MPS format or want full control over the submit/poll cycle:
OptClientV1 client = OptClientV1.newInstance("https://your-service-host");
// Submit an MPS file
byte[] mpsData = Files.readAllBytes(Path.of("model.mps"));
String response = client.putOnQueue(mpsData, "MPS", false); // false = minimise
// Or use the parsed variant
Map<String, Object> parsed = client.putOnQueueParsed(mpsData, "MPS", false);
String key = (String) parsed.get(OptClientV1.KEY);
// Poll until done
Map<String, Object> poll = client.pollResultParsed(key);
while ("PENDING".equals(poll.get(OptClientV1.STATUS))) {
Thread.sleep(100);
poll = client.pollResultParsed(key);
}
OptResult result = (OptResult) poll.get(OptClientV1.RESULT);
Model builder API
The OptModel builder supports real, integer, and binary variables, linear and quadratic constraints, and linear and quadratic objectives:
OptModel model = client.newModel();
// Variable types
OptVariable x = model.newRealVariable("x").lower(0).upper(100);
OptVariable n = model.newIntegerVariable("n").lower(0).upper(10);
OptVariable b = model.newBinaryVariable("use_option");
// Constraints with bounds
model.newConstraint("capacity")
.set(x, 2.5).set(n, 1)
.upper(50);
model.newConstraint("minimum")
.set(x, 1).set(b, -10)
.lower(0);
// Objective
model.objective().set(x, 3).set(n, 7);
// Solve
OptResult result = model.minimise().get();
Classes
| Class | Role |
|---|---|
OptClientV1 | HTTP client — submits models and polls for results |
OptModel | Model builder — variables, constraints, objective, serialisation |
OptVariable | Decision variable (real, integer, or binary) |
OptConstraint | Linear or quadratic constraint with bounds |
OptObjective | Objective function (linear or quadratic) |
OptExpression | Base for constraints and objectives — holds coefficients |
OptResult | Immutable result — solver state, objective value, solution vector |
ojAlgo Integration
If you already use ojAlgo, this is the most natural way to access the service. Wire the client into ExpressionsBasedModel and your existing models solve remotely without code changes.
Setup
You need both ojAlgo and the service client on the classpath. The integration uses ojAlgo's Optimisation.Environment to register the service as a remote solver:
OptClientV1 client = OptClientV1.newInstance("https://your-service-host");
Optimisation.Environment environment = Optimisation.newEnvironment();
environment.setRemoteSolver(client::putOnQueue, client::pollResult);
This works because the putOnQueue and pollResult method signatures match ojAlgo's Optimisation.ModelSubmitter and Optimisation.ResultPoller functional interfaces.
Solve remotely
Once the environment is configured, create models from it and submit them:
ExpressionsBasedModel model = environment.newModel();
Variable x = model.newVariable("x").lower(0).weight(3);
Variable y = model.newVariable("y").lower(0).weight(5);
model.addExpression("budget").set(x, 1).set(y, 1).upper(10);
// Solve via the service
Future<Optimisation.Result> future = model.submit(Optimisation.Sense.MAX);
Optimisation.Result result = future.get();
The model is serialised, sent to the service, solved by the best available solver on the server, and the result is returned — all through the standard ojAlgo API. Your model code does not change; only the environment setup is different from solving locally.
Develop locally, solve remotely
The intended workflow: during development, solve locally with ojAlgo's built-in pure-Java solvers. No server needed, no network dependency, fast iteration. When you deploy to production, configure the environment to point at the service and let it solve the same models at production scale, without installing anything.
// Development: solve locally (no environment setup needed) ExpressionsBasedModel model = new ExpressionsBasedModel(); // ... build model ... Optimisation.Result localResult = model.minimise(); // Production: same model, solve via the service Optimisation.Environment env = Optimisation.newEnvironment(); env.setRemoteSolver(client::putOnQueue, client::pollResult); ExpressionsBasedModel model = env.newModel(); // ... same model building code ... Optimisation.Result remoteResult = model.submit(Optimisation.Sense.MIN).get();
Supported problem types
The service handles the main classes of mathematical optimisation problems: linear programming (LP), quadratic programming (QP), and mixed-integer programming (MIP), including mixed-integer linear (MILP) and mixed-integer quadratic (MIQP). The server selects the best available solver for each problem type automatically.
Model formats
| Format | Description |
|---|---|
MPS |
Industry-standard Mathematical Programming System format. Supported by virtually all optimisation tools. Use this when submitting models from non-Java environments. |
LP |
CPLEX LP format. Also plain text, and often easier to read and hand-edit than MPS. Use either this or MPS when submitting models from non-Java environments. |
EBM |
ojAlgo's native ExpressionsBasedModel serialisation format. This is what the client library and the ojAlgo integration use on the wire — you do not choose it yourself, and you do not need to know about it unless you are calling the REST API directly with an ojAlgo model in hand. |