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.