Station X1 · Bilingual extensionOpen the adventure map
00Set up your toolsSetup01The starting lineSetup02Processes and mailboxesR1An Elixir data pipelineOptional reviewR2Read ErlangOptional review03Messages and timeouts04OTP rules for messages05A supervision tree that restarts06Put a limit on concurrency07When a BEAM node disappearsX1Agree on shared BEAM termsBilingual extensionX2Two-language partnersBilingual extension08A reliable job runner
Home/BEAM mainline/Station X1
X1
BridgeTwo-language challengeBilingual extensionElixirErlangBEAM

Agree on shared BEAM terms

Elixir and Erlang spell things differently. Stable terms let them keep one contract.

4checkpoints
About 5 hours · Try 4 sessionssplit it into sessions
QUESTION · ONE PROBLEM FOR THIS STATION

When an order receives pay, ship, cancel, and duplicate events, how can Elixir and Erlang follow the same state-machine protocol?

Start at the scene

The same paid value is not the same state on both sides

One side uses an atom while the other sends a string. One implementation permits a transition that the other rejects. After a duplicate event, the two copies of the order state diverge.

Evidence you can observe
  • The complete state-transition table
  • The raw terms exchanged across the language boundary
  • Bilingual contract tests for valid, invalid, and duplicate events
Why this station matters

The problem it solves

Comparing the same task in both languages is clearer than memorizing each one alone. Elixir writes :ok; Erlang writes ok. On BEAM, they are the same atom. Strings, modules, and exceptions still have real differences.

After this station

You will be able to

  • Translate common data, functions, and module calls from Elixir syntax to Erlang syntax and back

  • Explain which differences are only surface spelling and which values are already the same BEAM term

  • Use specs to record an agreed data shape and tests to catch changes to that agreement

Before you start
  • Finish Foundation in either language. You can read the other language beside it as you go
  • Know how to check that the same input produces the same output
One job, two ways to write it

See what it does before how it is written

Both sides receive the same atoms and return success or error tuples with the same shapes.

Current code
Elixir
# List the allowed order states as atoms
defmodule Order do
  @type state :: :new | :paid | :shipped

  # Return a new state on success and a clear error tag on failure
  @spec transition(state(), atom()) ::
          {:ok, state()} | {:error, :invalid_transition}
  def transition(:new, :pay), do: {:ok, :paid}
  def transition(:paid, :ship), do: {:ok, :shipped}
  def transition(_, _), do: {:error, :invalid_transition}
end
DESIGN DECISION

Why this shape?

Why align terms before making cross-language calls?

Choice

Elixir and Erlang share BEAM terms. Agree on atoms, tuples, binaries, and error results before crossing the boundary.

Cost and boundary

A shared VM does not make structs, records, and text conventions automatically compatible.

A FAMILIAR POINT OF VIEW

Coming from Java, Python, or JavaScript

Java

Familiar starting point
Two JVM languages sharing bytecode and an object model.
What BEAM changes
Elixir and Erlang share BEAM terms and module/function/arity, while surface conventions can still differ.
False friend
A record and struct are not one public DTO.

Python

Familiar starting point
Modules passing dicts, tuples, and bytes directly.
What BEAM changes
Shared terms make calls direct, while patterns make boundary shapes explicit.
False friend
A charlist and binary can both look textual but are different terms.

JavaScript

Familiar starting point
Modules sharing objects or exchanging JSON.
What BEAM changes
Code in one VM needs no JSON step, but it still needs a stable term contract.
False friend
The ability to pass internals is not a reason to expose them.
LAB
Hands on

Pass the same data both ways

About 15–25 minutes

Call both modules in both directions and compare atoms and tuples after they cross the language boundary.

  1. 01

    Put order.erl in the Mix project's src/ folder and compile the project

  2. 02

    In IEx, call :order.transition(:new, :pay) and record the return value

  3. 03

    From Erlang, call 'Elixir.Order':transition(new, pay) and compare the data shapes

Copy into the terminal and press Enter
# Compile the current Mix project and open IEx in its environment
iex -S mix
What you should see
  • Both sides receive the same tuple term represented by {ok, paid}
  • Erlang can find and call an Elixir module by using its real module atom
Break it on purpose

Change the atom paid on one side to the string "paid". The text looks alike, but the term type differs, so matching fails.

What this shows

Ordinary BEAM terms such as numbers, atoms, and tuples can pass directly between modules in the two languages.

What this does not show yet

Passing ordinary terms does not mean strings, Elixir structs, Erlang records, and exceptions need no extra agreement.

Meet the words

Key ideas in this code

01

Atom mapping

An atom is a fixed label. Elixir's :ok and Erlang's ok are the same atom. An Elixir module name is also an atom beginning with Elixir..

02

Module call

A call is identified by its module name, function name, and number of arguments. That number is called arity.

03

Agreed data format

Two modules agree on the shapes of inputs, returns, and errors. Across languages, these agreements matter more than surface spelling.

04

term

A piece of BEAM data is a term: a number, atom, tuple, list, map, and more. Ordinary terms can pass directly between both languages.

Name the shape in the code

Design patterns used here

01

State Machine

Define the allowed next state from the current state and event.

02

Command

Represent each pay, ship, or cancel event as an explicit command term.

03

Contract Test

Run both implementations against the same transition table.

Think it through

Which real module name does Erlang usually use to find the Elixir module Foo?

Your turn

An order-state relay

Add cancellation, refunds, and repeated events. Draw the state-transition table first, then implement it in both languages.

Hint 1Take the first step

Draw the states and events before writing code.

Hint 2Make it a little smaller

Decide whether a repeated event keeps the same state or returns an error, and write down the idempotency rule.

Hint 3You are close

Use pure functions to make the rules clear before hiding an unfinished idea inside a process.

Hint 4Work backward from the finish

Choose one success signal and write the smallest test for it. If the computer cannot show the result, rewrite the signal as something you can truly observe.

Ready to move on when
  • Both implementations return terms with the same shapes

  • A forbidden state change returns a clear error tag

  • Specs and tests cover every path in the state table

Take these with you

Remember three things

  1. 1

    Agree on input and return term shapes before choosing a language to express them.

  2. 2

    Elixir module names and aliases still become module atoms when they reach BEAM.

  3. 3

    Text, structs, records, and exceptions are the easiest places for the two sides to misunderstand each other. Agree and test them clearly.

Read a little more

Visit the original sources

Erlang interoperabilityTypespecs
Station completeYou ran the experiment and thought through the answer. Save this station.