05 Tierwise open source · pre-release · Java 17+ · Node 20+

Every answer explains itself.

Tierwise is a type checker and evaluator for layered business configuration. You declare your own levels, keys and decisions. A merge rule on every key, explicit all or any over collections, and provenance on every answer make the result mean one thing, the same thing, in Java and in JavaScript.

git clone https://github.com/djkeshawa/visp-tierwise

Pre-release. Engineering is complete and hosted CI is green, but 0.1.0 isn’t on Maven Central or npm yet. Build from source today.

Checkout serviceresolve

checkout.pickup.lead_hours division: south · seller: acme

contributions, in path order
Tierwise8 hours
  1. global2set
  2. divisionsouth · 6, the most specific level on the pathset
  3. selleracme · 8, an axis across the levelssource
  4. ruleoverride, then combine_across_axes: max between the levels and the seller
  5. bundlesha256:ab1cd3a9… · the same bytes give the same answer in Java and in Node.
From the commerce sample. Every result names each contributing scope, the winner and the bundle version.
  1. 1Declaretierwise.yamlLevels, keys with a merge rule each, decisionsyour repo
  2. 2Validatetierwise validateTypes, typos, admission and lint, with suggestionsfails CI
  3. 3Testtierwise testYour examples, each with an expected effectfails CI
  4. 4Compiletierwise compileCanonical bundle, named by its SHA-256bundle.json
  5. 5Evaluateresolve · decideJava or Node, in process; browsers for displayyour services
A team can stop at any stage: validate only, evaluate in process, add the Spring Boot starter or Node loader, or publish through the optional control-plane server.

Merge rules

One rule per key, said out loud.

Each key declares how values from different levels combine. Choose a rule to see three levels resolve to one answer.

  1. global2
  2. division6
  3. site4most specific
pickup lead hours4

The most specific value on the path wins. Needs a default or a root value.

  1. global{header: "Acme", footer: "Thanks"}
  2. division{footer: "Merci"}
  3. site{show_logo: true}
receipt3 fields

{header: "Acme", footer: "Merci", show_logo: true}. Fields merge recursively; lists are replaced.

  1. globaltrue
  2. divisionfalseveto
  3. sitetrue
express checkout enabledfalse

AND across the path: one false anywhere vetoes it. A capability, so lower levels can only narrow.

  1. globalfalse
  2. divisionfalse
  3. sitetruegrants
live chat enabledtrue

OR across the path: one true turns it on.

  1. global[GROUND, EXPRESS, PICKUP]
  2. division[GROUND, PICKUP]narrows
  3. site[PICKUP, EXPRESS]
shipping methods[PICKUP]

The declared universe, intersected with every value on the path.

  1. plan[csv]
  2. org[pdf]
  3. workspace—
export formats[csv, pdf]

Every grant on the path adds to the set.

  1. global500
  2. division100smallest
  3. site250
max order lines100

The smallest bound wins, so a site can’t raise a division’s limit.

  1. global10
  2. division25largest
  3. site—
burst allowance25

The largest value wins. If nothing on the path sets it, the result is absent.

Illustrative values. The six commutative rules can’t declare a default; override and deep_merge depend on order and need one. There is no null, no coercion and no float.

Decisions

“All” or “any”, never implied.

Decision tables test facts about an entity and its children. A rule over a collection must say whether every child has to match or just one, and every answer carries the trace of the rules it checked.

- id: credit-hold-allowed-stock
  when:
    customer.active: true
    customer.credit_hold: true
    item.stock_type: { all_in: $allowed_on_credit_hold }
  then: ALLOW
decide · checkout.order_submissionDENY
  1. factscredit hold · items STOCKED, DROP_SHIP
  2. bindingdivision south · allowed on hold: STOCKED, SPECIAL_ORDER
  3. rule 1good-credit · failed on customer.credit_hold✕
  4. rule 2credit-hold-allowed-stock · failed on item.stock_type (all_in)✕
  5. defaultNo rule matched, so the policy default applies: DENY. One drop-ship item is enough to fail “all”.
A mixed basket on credit hold, from the commerce sample’s own examples.
Typos fail
A misspelled key, node or fact is an error at build time and at run time, with a suggestion, never a silent no-op: quota.max_itmes fails with the suggestion quota.max_items.
No guessing
What happens on an unknown node, a missing fact or a provider outage is declared in the schema, not defaulted. Presets can tighten checks but never turn off an engine guarantee.
Provenance
Every result names each contributing scope, the winner, the matching rule and the bundle version. There is no API that reads one level’s raw value.
Canonical
The same sources compile to the same bytes, and a bundle’s name is the SHA-256 of its canonical content. Every loader checks it.
Conformance
1,106 normative clauses, 576 evaluation vectors and 582 compile cases, passed by both evaluators. The nightly differential run has found no differences.
Fast
In-process resolve and decide take about 4 to 7 µs at p99 on a bundle of 1,000 keys and 50 features. The target is under 100 µs.
Keeps running
The Spring Boot starter and Node loader refresh by ETag, swap atomically and keep the last good bundle when a refresh fails.
If you need…Use
Release toggles, experiments, kill switchesA feature-flag tool. Tierwise can sit behind OpenFeature as a provider.
Authorization: who may do what to which resourceCedar or OPA
A general-purpose configuration languageCUE, Pkl or Jsonnet
Typed settings across levels you define, a merge rule per key, entity-aware decisions, provenance, Java and JavaScriptTierwise

Status

Built, tested, not yet shipped.

Specification 0.1 is frozen and bundle format 1 is implemented in Java and JavaScript. The 0.1.0 release is waiting on publishing steps, not code.

Done

  • Hosted CI green on the Java reactor and the JavaScript packages
  • Nightly differential run: 10,000 cases, no difference
  • Release dry run: native binaries for Linux, macOS and Windows, Docker images, SBOMs
  • Both quickstarts pass on a fresh machine

Before 0.1.0

  • Maven Central namespace and npm scope
  • Signing keys for artifacts
  • The first scheduled nightly run
  • Publishing the binaries, images and docs site

Get started

From a clone to a checked bundle.

Building needs a full JDK 21; the CLI and core then run on Java 17 or newer. The JavaScript evaluator runs on Node 20+ and in browsers.

Full quickstart
  1. 1

    Build the CLI.

    Until 0.1.0 is published, the CLI is a fat JAR you build from the repository.

    git clone https://github.com/djkeshawa/visp-tierwise && cd visp-tierwise
    (cd java && ./mvnw -B -ntp -DskipTests package)
    alias tierwise="java -jar $(ls "$PWD"/java/tierwise-cli/target/tierwise-cli-*-all.jar)"
  2. 2

    Start a project and check it.

    init writes a minimal, valid project from a built-in preset. Try misspelling a key and run validate again.

    tierwise init --preset=entitlements my-config
    tierwise validate my-config
    tierwise test my-config
  3. 3

    Compile, then evaluate.

    Load the bundle with tierwise-core in Java or @tierwise/evaluator in Node, and ask it questions.

    tierwise compile -o bundle.json my-config

Keep going