Station X2 · 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 X2
X2
BridgeTwo-language challengeBilingual extensionElixirErlang

Two-language partners

Calling each other inside one BEAM is easy. Agree on the exchanged data first.

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

Between an Elixir API and an Erlang worker, how do we fix the text, data-shape, and error protocol so record or Unicode changes do not spread?

Start at the scene

One field was added to a record, and Elixir's positional reads all broke

Erlang changes a private record while Elixir keeps reading tuple positions. The boundary also mixes binaries and charlists, so one small change spreads into many failures.

Evidence you can observe
  • Raw boundary terms and guards
  • Unicode round-trip tests for Chinese text, emoji, and empty text
  • Bilingual contract tests for success, business errors, and process exits
Why this station matters

The problem it solves

The two languages can call each other directly inside one BEAM, but text, records, and structs have different shapes. Keep conversions at the boundary and agree on success and failure terms.

After this station

You will be able to

  • Call Erlang from Elixir and call Elixir from Erlang with the correct module atom

  • Convert Elixir Strings, binaries, and charlists explicitly at the boundary

  • Turn exceptions and differing returns into stable results that both sides can pattern match

Before you start
  • Finish the shared-terms station and know that :ok and ok name the same atom
  • Use Mix and Rebar3 to run a small project and its tests
One job, two ways to write it

See what it does before how it is written

Keep conversion at the edge. Use tagged tuples in the core protocol so charlists do not spread through the project.

Current code
Elixir
# The Elixir API validates input and converts boundary data
defmodule Scheduler do
  @spec submit(binary(), map()) ::
          {:ok, reference()} | {:error, atom()}
  def submit(queue, payload) when is_binary(queue) do
    # The Erlang worker expects a charlist and a key-value list
    :job_worker.submit(
      String.to_charlist(queue),
      Map.to_list(payload)
    )
  catch
    # Turn an exit across the boundary into a stable error tuple
    :exit, reason -> {:error, normalize_exit(reason)}
  end
end
DESIGN DECISION

Why this shape?

Why add an adapter layer?

Choice

An adapter translates private records or structs into stable terms, keeping change at one boundary.

Cost and boundary

It adds a small layer now and prevents one internal field change from spreading through every caller.

A FAMILIAR POINT OF VIEW

Coming from Java, Python, or JavaScript

Java

Familiar starting point
Adapters and DTOs between modules or JVM languages.
What BEAM changes
The adapter translates records, structs, binaries, and error tuples.
False friend
Do not read a private record by tuple position.

Python

Familiar starting point
A boundary function translating third-party objects into dataclasses or dicts.
What BEAM changes
Stable terms give two BEAM languages one protocol.
False friend
Translate exceptions at the adapter as well.

JavaScript

Familiar starting point
An API schema or anti-corruption layer.
What BEAM changes
The boundary may stay inside one VM and still deserves a fixed shape.
False friend
Internal convenience is not a public contract.
LAB
Hands on

Why do the two versions of jobs not match?

About 15–25 minutes

First pass an Elixir String to a worker that accepts only a charlist. Then convert it in an adapter and add tests.

  1. 01

    Pass "jobs" from Elixir and record what the guard or function clause tells you

  2. 02

    Use String.to_charlist/1 in the adapter before calling the Erlang worker

  3. 03

    Add a round-trip test with a non-ASCII queue name and confirm that the text stays unchanged

Copy into the terminal and press Enter
# Run only the ExUnit file for the language boundary
mix test test/interoperability_test.exs
What you should see
  • A binary and a charlist match different guards
  • After an explicit boundary conversion, both sides meet the agreement
  • A Unicode test catches code that wrongly treats text as single bytes
Break it on purpose

Make Elixir read an Erlang record by tuple position, then add a field to the record. Watch the coupling break.

What this shows

Explicit text conversion at the boundary keeps the String, binary, and charlist agreement consistent.

What this does not show yet

One successful path cannot show that every success, failure, and edge return from a third-party library uses the same text format.

Meet the words

Key ideas in this code

01

charlist

A list of integers that are character code points, common in Erlang APIs. An Elixir String is usually a UTF-8 binary.

02

record

Erlang expands a record into a tuple at compile time. Across languages, use public functions or maps instead of depending on tuple positions.

03

exception boundary

The agreement for exceptions, exits, and throws in both languages. The boundary turns them into stable error tuples.

Name the shape in the code

Design patterns used here

01

Adapter

Centralize text, record, struct, and error conversions in one boundary module.

02

Anti-Corruption Layer

Keep one language's private representation out of the other side's core code.

03

Data Transfer Object

Exchange simple, stable tagged terms or maps.

Think it through

Which return agreement is usually most stable for long-term work between the two languages?

Your turn

Share one task queue

Let Elixir provide the API and validation while an Erlang gen_server owns the queue. Tests must truly cross the language boundary.

Hint 1Take the first step

Write a few concrete success and failure terms before building the modules.

Hint 2Make it a little smaller

Keep all text and error conversion inside one thin adapter module.

Hint 3You are close

Let ExUnit call the Erlang worker, and let EUnit call the Elixir normalize function.

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
  • Each language has one clear, real responsibility

  • Binary, charlist, and exception conversions stay at the boundary

  • Two-way tests fail at once if either side breaks a return shape

Take these with you

Remember three things

  1. 1

    Sharing BEAM makes calls easy. A clear data agreement keeps long-term cooperation easy too.

  2. 2

    Text forms, records, and exceptions cause the most confusion and need careful boundary handling.

  3. 3

    An adapter should be thin, focused, and genuinely tested from both directions.

Read a little more

Visit the original sources

Erlang libraries from ElixirStrings and binaries
Station completeYou ran the experiment and thought through the answer. Save this station.