jvoegele

jvoegele

Bond (Design by Contract for Elixir) - 1.0.0 released

Bond 1.0.0 is out — the first stable release. :tada:

After three release candidates and a good deal of dogfooding, Bond has reached 1.0.0. The SemVer stability guarantees described in the docs are now in force: the public API surface is frozen, and changes to it follow a real deprecation policy from here on.

def deps do
  [{:bond, "~> 1.0"}]
end

What Bond is

Bond brings Design by Contract to Elixir. You write a function’s obligations and guarantees right next to the code they constrain, and Bond checks them at runtime with failure messages that tell you exactly what was violated and why:

defmodule Account do
  use Bond

  @pre sufficient_funds: amount <= account.balance, positive: amount > 0
  @post drawn_down: result.balance == account.balance - amount
  def withdraw(account, amount) do
    %{account | balance: account.balance - amount}
  end
end

Beyond @pre/@post, Bond gives you @invariant for struct modules (a property that must hold of every instance), check/1,2 for inline assertions, and old/1 for referring to a value as it was on entry. Contracts are conditionally compiled per environment — each kind (:preconditions, :postconditions, :invariants, :checks) can be true, false, or :purge, and :purge strips the contract code entirely so there is zero runtime cost in production.

Bond stays in its lane: it doesn’t replace typespecs, set-theoretic types, or ExUnit. It complements them with runtime-checked assertions over parameters, results, and state.

Because @invariants are machine-checkable, Bond can also drive them: Bond.PropertyTest.invariants_hold/2 runs randomized sequences of constructor/transformer/observer operations (via StreamData) and uses your invariants as the oracle across every reachable state — property-based testing for free, once you’ve stated the invariant. (Details in the docs.)

The getting-started guide is the best on-ramp.

What changed across the release-candidate cycle

The rc.1 announcement covered the bulk of the feature set — invariants, the concurrency guide, conditional compilation, library coexistence, corrected predicate typespecs. A few things landed after it that weren’t yet announced here:

  • rc.2 — Bond can share a single module with other libraries that override Kernel.@/1 or wrap your functions (Norm, anything built on decorator): use Bond, at_annotations: false plus tolerance of externally-generated override clauses. Contract labelling was also unified on the single keyword-list form (@pre positive: x > 0).
  • rc.3 — the property-testing entry point was split into two clearly-named macros: contract_holds/2 for a single function and the new invariants_hold/2 for the stateful module-sequence form above.
  • rc.4 — a soundness fix for @invariant on heterogeneous multi-clause functions: a struct clause’s pre-invariant could be silently skipped when a sibling clause matched a non-struct value. Now fixed.

The full history is in the changelog.

What 1.0.0 means for you

The point of 1.0.0 is that you can now depend on Bond’s public surface with confidence:

  • Every name covered by the SemVer contract is enumerated in the public API surface guide — macros, attributes, predicates, the test helpers, telemetry, error structs, config keys, types. Internal namespaces are explicitly carved out.
  • The stability guarantees guide spells out what patch / minor / major mean in practice, what’s explicitly excluded (compile-error text, generated-code shape, exception message text), and the deprecation policy.
  • Compatibility is verified across Elixir 1.16–1.19 in CI.

What’s next

With 1.0 stable, the next thread of work is contract inheritance: letting a behaviour declare @pre/@post on its callbacks so that implementing modules inherit those contracts automatically (with protocols to follow). This is design-in-progress, not a promise of timing — if the idea interests you, early discussion is very welcome on the issue tracker.

Thanks

Bond is meaningfully better for the feedback from the original 2024 thread and the RC threads — thank you to everyone who kicked the tires, filed issues, and pushed back on the design. That input is exactly what the RC window was for, and it shaped what shipped today.

Feedback, bug reports, and ideas remain welcome here or at Issues · jvoegele/bond · GitHub.

Links

Original library announcement: Bond - Design by Contract for Elixir

RC1 announcement: Bond (design by contract for Elixir) - 1.0.0-rc.1 released

Most Liked

Petr

Petr

This looks really great :clap: Thank you for creating it.

Question: how could the predicates be abstracted? Meaning, If I have a predicate that applies to a certain function argument that is accepted across 3 different functions in a module, I’d like to declare the predicate only once for the argument and reuse it.

jvoegele

jvoegele

Hi @Petr, thank you for the kind words!

The idiomatic way to do this is to define the predicate once as an ordinary function in your module and then call it from the @pre (or @post) of each function that needs it. Contract expressions are just plain Elixir, so any function in scope — including your own — can be called inside them:

defmodule Mailer do
  use Bond

  @pre valid_recipient: valid_email?(to)
  def send_welcome(to, name) do
    # ...
  end

  @pre valid_recipient: valid_email?(to),
       nonempty_subject: subject != ""
  def send_notification(to, subject, body) do
    # ...
  end

  @pre valid_recipient: valid_email?(to)
  def unsubscribe(to) do
    # ...
  end

  # The reusable predicate — declared once, called from any contract.
  def valid_email?(address) do
    is_binary(address) and String.contains?(address, "@")
  end
end

The validation logic lives in one place; each function just references it. The label (valid_recipient:) is what shows up in the error message and generated docs, so you can keep it consistent or vary it per call site, or even omit it entirely. The predicate can be a private defp if you’d rather not export it — it still resolves inside contracts since the generated checks run in the same module.

If the argument in question happens to be a struct defined in that same module, there’s an even more automatic option: @invariant. Invariants encode predicates on the struct itself and are checked on entry to and exit from every public function in the module that takes or returns the struct — so you don’t repeat them per-function at all. The trade-off is that they apply module-wide (to every public function touching the struct), not to a hand-picked subset, and they’re struct-only. For the general “reuse across a few specific functions” case, the predicate-function pattern above is the way to go.

There’s a worked @invariant example — BoundedStack, with non_negative_capacity and size_within_capacity invariants — in the @invariant section of the docs if you’d like to see it in action.

nicflower

nicflower

this is brilliant, great job!

I have one question, I see that you setup a way to toggle the functionality in prod. Do you think the checks should be turned off in prod? What use case do you have in mind?

Where Next?

Popular in News & Updates Top

zachdaniel
:police_car_light: New AshAuthentication Installers :police_car_light: The AshAuthentication igniter installers are released! Today on s...
New
DominikWolek
Last Friday we released Membrane Core 0.11.0 :tada: A pipeline’s lifecycle has changed: pipeline is now spawned with a dedicated supervi...
New
jjcarstens
Do you like Hacktoberfest? Also enjoy working with Nerves and want to contribute? Fantastic! :tada: :beers: Here are some potential sta...
New
fhunleth
We recently released Nerves 1.6 and corresponding updates to the Nerves new project generator, nerves_bootstrap and our official systems....
New
bartblast
Hologram v0.7.0 is out! This is a milestone release for the Elixir-to-JavaScript porting initiative. 49 contributors ported 150 Erlang fu...
New
Jskalc
LiveVue v1.0 released After four release candidates and a lot of community feedback, LiveVue 1.0 is stable :tada: I’ve built a dedicated...
New
zhenfeng-zhu
Nex - A minimalist web framework for indie hackers and startups A Note Before We Begin This document records my thoughts during the dev...
New
fhunleth
We’ve released Nerves v1.3.0 with support for Elixir 1.7 and Distillery 2.0! We encountered a few bumps over the past week, but it’s look...
New
zachdaniel
Ash Framework 3.0: Official Release! I’m here with the fine folks at Gig City Elixir, pushing the button live on stage :sunglasses: T...
New
hugobarauna
Welcome to the second day of Livebook Launch Week! :tada: Today we will discuss all the new Machine Learning capabilities in Elixir and ...
New

Other popular topics Top

senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New
AstonJ
Posting this to see if we can make things easier for people to get into Neovim. If you use Neovim and have a favourite distro please let ...
New
albydarned
Hello all! I am typing this post from my new MacBook Pro with the M1 chip. I’m loving it so far, and will probably use it as my daily dr...
New
greenz1
I have a phoenix application from which a user can download multiple(5-6) files of size 1MB. I couldn’t find anything related to sending ...
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID&lt;0.412.0&gt; terminating ** (Postgrex.Error) FATAL...
New
ovidiubadita
Hey all, I discovered Elixir and I love it. I always wanted to learn a functional programming and I intended to go for Haskell, but afte...
New
pmjoe
I have a relationship of love and hate with Elixir. Lots of things are just absolutely right, but there are some things that are kind of ...
New
axelson
This post is a wiki (feel free to hit the edit button near the bottom right of this post to add your own changes!) This post collects co...
239 47930 226
New
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
New
svb
Hi! Currently I want to submit a form by pressing the Enter key. However, since my input field is of type “textarea” this is just adds a...
New

We're in Beta

About us Mission Statement