Prism

v0.1 · pre-release · Apache-2.0

Topology is a compile flag.

Develop one modular monorepo against distributed semantics. Verify its boundaries on every commit. Compile it — deterministically — to a monolith or to microservices. Same code, same tests, both shapes.

$ mvn prism:compile -Dprism.topology=services

The problem

“We'll split it later” is a promise five years of coupling always breaks.

Teams correctly start with a monolith and plan to extract services when scale demands it. Then the coupling accumulates, and “later” arrives as a two-year rewrite. The monolith-vs-microservices decision gets made years before the information needed to make it exists — and unmaking it costs enormously.

Prism makes the promise enforceable. Boundaries are declared, not inferred — and a verifier guarantees in CI that they hold. Deployment topology becomes a reversible, per-environment compile flag instead of a bet placed on day one.

How it works

Distributed semantics from day one. Running as a monolith is free.

01

Declare modules and contracts

A module is a package. A contract is its only public surface — and it must admit failure by type. The verifier rejects anything else.

PaymentContract.java
@Contract
public interface PaymentContract {

    @Deadline(millis = 2000)
    Result<Receipt> charge(ChargeRequest request);
}
02

Call it like plain Java

Inject the interface. The Result type forces you to handle failure — which is exactly what makes the same code correct over a network.

OrderService.java
return payment
    .charge(new ChargeRequest(customerId, amount))
    .map(receipt -> placeOrder(receipt))
    .fold(
        order -> accepted(order),
        failure -> switch (failure.kind()) {
            case TIMEOUT, UNAVAILABLE -> retryLater();
            case REJECTED -> tellUser();
            // ...
        });
03

Verify on every commit

Six deterministic rules fail the build on boundary violations — including transactions that span modules. Rejected, not “handled”.

mvn prism:verify
ERROR [transaction-containment] OrderService.java:42
Transactional method OrderService.place calls contract
PaymentContract of module 'payment' — transactions
must not span module boundaries
04

Compile to either topology

One deployable for dev and small scale. N services — with generated HTTP bindings, Dockerfiles, and compose — when a module needs its own life. Byte-identical on every run.

prism.yml
modules:
  order: { port: 8081 }
  payment: { port: 8082, deadline-ms: 2000 }
packs:
  -  builtin:gateway
  - builtin:observability

What you get

A compiler, a verifier, and an honest set of escape hatches

Declared boundaries

Modules are packages you declare, not guesses a tool makes. Verification is decidable; inference never was.

Fallible by type

Cross-module calls return Result<T> in both topologies. In monolith mode the failure branches simply never fire.

Six verification rules

Boundary integrity, contract fallibility, transaction containment, data ownership, event honesty, declaration sanity — in CI, deterministic.

Deterministic compiler

Same monorepo, byte-identical output — enforced by compile-twice-compare-bytes tests in Prism's own suite.

Template packs

Gateway, observability, Kafka — expressed in a structural template language anyone can extend. The built-ins contain zero Java.

The customization ladder

Annotations → overlays → runtime bindings → ejection. Every escape hatch lives in the monorepo; generated output is never hand-edited.

The guarantee

You were writing distributed-safe code all along.

The classic monolith-to-microservices failure is that the semantics change under the code: in-process calls can't time out, can't partially fail, and are transactional for free. Prism flips the abstraction — you always code against distributed semantics, and the in-process binding is just an execution optimization that happens to never fail.

Kill a service's dependency and it stays up, returning the failure your code already handles — because the type system made you handle it on day one.

the same code, both topologies
// monolith:  local dispatch — never fails
// services:  HTTP + deadline — can fail
Result<Receipt> result = payment.charge(request);

// either way, you wrote the failure path:
result.fold(ok -> ship(), failure -> degrade());

Develop like a monolith.
Deploy like microservices.

Open source, Apache-2.0. Java 21+. The verifier alone is worth the install — adopt it without ever compiling.