Object-Oriented Prompt Engineering for JavaEst. 2026 · MIT License
PrOOPt

“A Java library that keeps LLMs on a need-to-know basis.”

Chapter Three

Developer Tutorials

Six short lessons. Each one builds on the last, and none of them requires a whiteboard.

Lesson 1 — Autoboxing, or: Your Return Type Is the Contract

In Lesson 0 (the Getting Started page — you did read it, we saw you) you wrote a method that returned null and got a LocalDate back. The machinery behind that is the PrOOPtAutoBoxer, and it is patient with models the way a good teacher is patient with students.

  • Primitives — strips commas, strips trailing .0, extracts the first number from prose. If the model says “The answer is 42, naturally,” you get 42.
  • Booleans — yes/no, true/false, 1/0, case-insensitive.
  • Enums — scans the prose for the first matching constant name, case-insensitive.
  • java.time — ISO-8601 parsing for LocalDate, LocalDateTime, ZonedDateTime.
  • Collections, Maps, POJOs — Jackson JSON, with markdown fence stripping (because models do love a good ```json fence).

The practical upshot: design your @PromptFunction signatures the way you design any API. The richer and more honest the return type, the more PrOOPt can enforce on your behalf.

@PromptFunction(
    model = ModelTier.CLOUD_FAST,
    prompt = "Classify the urgency of this support ticket: {ticket}"
)
public Urgency classify(String ticket) {
    return null; // Urgency is an enum: LOW, NORMAL, HIGH, FIRE
}

Lesson 2 — @CodeFunction as Knowledge Injection

Every @CodeFunction does two jobs. The obvious one: it computes something correctly, every time, for free. The subtle one: it extends the LLM's effective capability beyond its training boundary. Your proprietary risk model, your internal scoring formula, your novel algorithm — none of these exist in any training corpus. Register them as @CodeFunctions and the orchestrator can reason about them and invoke them as first-class tools, without ever seeing inside.

@CodeFunction(
    description = "Computes the bank's internal credit score "
                + "from a structured application"
)
public static CreditScore scoreApplication(LoanApplication app) {
    // Twenty years of actuarial wisdom, zero tokens.
    return CreditModel.evaluate(app);
}

The LLM reads the loan application, classifies the risk tier, and decides the formula is needed. The @CodeFunction computes it deterministically. The LLM never sees the formula — only the result. Your trade secret is used but never exposed.


Lesson 3 — The Orchestrator and Its DAG

When you call orchestrate(...), the orchestrator runs a tidy little pipeline before any work happens:

User input
   │
   ▼ Phase 1 — Discovery (LOCAL model)
List<String> capabilities
   │
   ▼ Phase 2 — Matching (TF-IDF cosine similarity)
List<ToolDescriptor> (≤ maxTools)
   │
   ▼ Phase 3 — Planning (CLOUD_FAST model)
ExecutionPlan (JSON)
   │
   ▼ Wave-parallel execution
Result

The plan is a DAG. Independent subexpressions fire concurrently; cross-stream dependencies resolve non-blocking via CompletableFuture. You declare appetite for parallelism once, on the orchestrator:

@PromptOrchestrator(
    prompt = "You are a legal analyst.",
    planMode = PlanMode.DYNAMIC,
    model = ModelTier.CLOUD_ADVANCED,
    parallel = true
)
@PromptFunctionScan
public class LegalAnalyzer extends BaseOrchestrator {
    public static void main(String[] args) {
        PrOOPt.run(LegalAnalyzer.class, args);
    }
}

A worked example from the library itself: ask the Linear System solver for help and the LLM does not solve the equations. It identifies the problem type, selects the strategy, and plans a DAG where Gaussian elimination runs as a @CodeFunction — deterministic, reproducible, provably correct. The LLM planned the solution. Java computed it. The result is guaranteed by the compiler, not by probability.


Lesson 4 — PromptStream, the Pipeline

Sometimes you do not need a planner; you already know the order of operations. PromptStream gives you a fluent pipeline where deterministic and LLM-backed steps snap together like train cars:

String summary = PromptStream.of(contractText)
        .pipe(proxied::normalizeWhitespace)   // @CodeFunction
        .pipe(proxied::generateSummary)       // @PromptFunction CLOUD_ADVANCED
        .withTimeout(5_000)
        .execute();

Each pipe keeps its own annotation's governance: the normalizer runs as plain Java, the summary call goes to the cloud tier it declared, and the whole journey lands in the audit log under one trace id.


Lesson 5 — Plan Modes: STATIC vs DYNAMIC

@PromptOrchestrator(planMode = ...) controls whether the execution plan is cached and reused. This is the difference between paying a planner once and paying a planner hourly.

ModeCloud plan-generation callsBest for
STATIC (default)1 total (first call) Batch and repetitive workloads — 10,000 loan applications share one plan
DYNAMIC1 per call Conversational agents, genuinely varied inputs

Under STATIC, the cached artifact is a plan template with ${input} placeholders retained. On each run, PlanInstantiator binds the live input and assigns a fresh trace id. A warm hit skips discovery and planning entirely:

[PROOOPT][PLAN_CACHE][HIT] key='Analyze this loan…' similarity=1.00
        → skipping Cloud LLM
[PROOOPT][ORCHESTRATOR][SUMMARY] trace=… mode=STATIC cached=true
        plan_generation=0ms cloud_calls=1 …

Plans are memoised by input hash, and the cache strategy can match exact text, semantic similarity, or intent. If the phrase “a JIT compiler for cognition” just crossed your mind, you will enjoy the PromptJIT use case.


Lesson 6 — Keeping Secrets: @SensitiveData and the Audit Log

Mark a parameter as sensitive and the audit log replaces the value with your label. The model still receives the real value — the log is what stays clean:

@PromptFunction(model = ModelTier.CLOUD_ADVANCED, ...)
public String generateSummary(
        @SensitiveData(label = "***CONTRACT-TEXT***") String text) {
    return null; // PrOOPt supplies the result
}

And when data must not reach the cloud at all, do not write a policy document. Write an annotation. @PromptFunction(model = ModelTier.LOCAL) means this data does not leave the JVM. Period. The developer declared it. The annotation enforces it. The audit log proves it. Your compliance officer can read the code and verify the boundary — possibly the first time anyone has handed them good news in annotation form.

Audit entries are written by the io.github.argonizer.prooopt.audit logger with daily and 50 MB rotation, with trace ids and per-run cost estimates. Every decision is reconstructable. The compliance artifact is a by-product of execution.

Graduation

That is the core curriculum. You can now declare authority zones, inject proprietary knowledge as tools, let an orchestrator plan without computing, pipeline steps, amortise planning costs, and keep your secrets out of the logs. When you are ready to see how far these primitives stretch, the Use Cases section is where things get genuinely strange — in the best way. Or meet the other half of the family in PrOOPt States.