BKIR is a language- and transport-neutral intermediate representation of what a feature or business rule does —its contract, its flow, its effects and all of its outputs— so you can regenerate it in another language or framework while preserving what is observed from the outside.
What it is
BKIR captures features extracted from source code (EJB/RMI, REST, SOAP, gRPC, RPC…) or
designed from scratch. It does not model the internal architecture: it models the
observable boundary. The unit is the Unit (one
.bkir file), and Units reference and orchestrate one another.
Design: define applications (ideally from a graphical tool) → BKIR → multiple implementations. Extraction: read code → BKIR; what is repetitive may be promoted to a reusable Unit.
Generated code is disposable and never hand-edited. What is not captured is lost: stable identity, deterministic and diffable generation, files in git.
From the contract + the outputs + the guards, tests (golden / property) are derived and run against the original and the regenerated code to prove they produce the same.
Why BKIR?
The fair question: if you want to go from EJB to Spring Boot, why not translate the code directly? Because direct translation transcribes the mechanism (the layers, the exception classes, the source idioms) and produces "Java written in Go". The IR captures only the observable behavior, and that unlocks advantages direct translation cannot offer.
Direct translation — one translator per pair:
EJB→Spring · EJB→Quarkus ·
EJB→Go · Spring→Go · …
→ N × M translators.
With BKIR — the IR in the middle:
[EJB, Spring, …] → BKIR → [Spring, Quarkus, Go, …]
→ N extractors + M generators. Adding a language is +1, not ×N.
Direct translation drags along the source's accidental architecture. BKIR preserves the contract, flow, effects and outputs: the generator produces code that is natural to the target, not a transliteration.
From direct translation you can only check "it compiles". From BKIR —contract + outputs + guards— equivalence tests are derived and run against the original and the regenerated code. The IR is what makes it possible to trust the result.
You review the behavior —a readable model, with stable ids and
provenance— not tens of thousands of generated lines. Diffable, versionable and a
basis for a graphical tool.
The output catalog, the transaction boundaries, the security requirement, the error codes… in the original they live spread across annotations, config and handlers. A direct translator would have to rediscover all of that for each target.
Spring Boot 1.5 → 6, EJB → Spring, on-prem → cloud: behavior is invariant; only
the binding changes. BKIR isolates the logic from the transport, so a framework change
is regenerating, not rewriting.
What is not formally captured is marked explicitly (foreign,
reviewed: false). Direct translation carries over what it recognizes and silently breaks
the rest.
Guiding principle
Collapse the internal mechanism, expand the observable variation.
Two internal paths that produce the same output are the same output. An exception is modeled only if it produces a distinct output at the boundary; its Java class or its mechanism is irrelevant.
What is in scope: the contract, the flow, the effects (writes, mutations, events) and their atomicity, because partial state is observed in later invocations. Decomposing into Units is allowed, but as semantic reuse, not as a reflection of the code's layers or classes: the generator decides whether to inline or invoke each reference.
The model
Each Unit declares a common envelope (bkirVersion, id,
kind, name, uses, source,
provenance) plus its kind-specific fields. The id carries a short,
stable prefix.
Domain terms + fact types (lightweight SBVR). Provides stable, readable names.
Reusable canonical type: Record, Enum, Union,
alias with constraints. No Java types.
Named business condition with a signature. Detection via
predicate (FEEL) or attempt (when you try and it fails).
Pure derivation: a FEEL expression, or a foreign block with a contract
(for algorithms that aren't expressed declaratively).
Read or effect against the environment (DB, service, filesystem, channel). Declares
target or channel and the attempt conditions it raises.
A referenceable messaging channel: payload + delivery
(guarantees). Connects producer → channel → consumer in a navigable way.
Orchestrator: input + flow + outputs. Exposed
via bindings (synchronous) or trigger (channel-driven).
Expressions (conditions, computations, guards) are written in FEEL (BKIR profile: names = identifiers). Operators, reserved words and functions in the language reference.
How it fits
operation at a glanceThe flow is a graph of nodes that matter to behavior; each one is inline or a reference to another Unit. Observable outputs live in a canonicalized catalog.
kind: operation
name: Savings account withdrawal
input:
cuentaId: { type: ID }
monto: { type: Money }
flow:
- decision: COND_MONTO_NO_POSITIVO # if (monto <= 0) throw ...
whenTrue: { to: OUT_MONTO_NO_POSITIVO }
- read: { bind: cuenta, use: IO_CUENTA_AHORRO_GET, args: { cuentaId: cuentaId } }
onCondition: { COND_CUENTA_NO_EXISTE: { to: OUT_CUENTA_NO_EXISTE } }
- decision: COND_SALDO_INSUFICIENTE
whenTrue: { to: OUT_SALDO_INSUFICIENTE }
- effect:
transactional: true # explicit atomic boundary
steps:
- bind: transaccionId
use: IO_RETIRO_REGISTRAR
args: { cuentaId: cuentaId, monto: monto }
- outcome: OUT_RETIRO_OK
outputs:
OUT_RETIRO_OK: { result: success, payload: { type: Record, fields: { transaccionId: { type: ID } } } }
OUT_SALDO_INSUFICIENTE: { result: error, code: "error.msg.savingsaccount.insufficient.balance",
message: "Insufficient account balance.", guard: COND_SALDO_INSUFICIENTE }
# … rest of the output catalog