Elixir 1 12
Release Overview
Elixir 1.12 released in May 2021 represents the minimum supported version for OSE Platform applications. This designation makes 1.12 the baseline release - the foundation upon which all OSE Platform Elixir code must run.
Three years of production use (May 2021 - May 2024) validated the stability of this release. Organizations running Elixir 1.12 in production today benefit from battle-tested features with well-understood edge cases. The release introduced practical improvements to configuration management and scripting capabilities while maintaining the backward compatibility guarantees Elixir users expect.
OSE Platform chose 1.12 as the minimum version requirement for several technical reasons. The Config.Reader module introduced in this version provides the configuration primitives needed for multi-environment deployments. Mix.install scripting capabilities enable automation scripts and deployment tools. Most importantly, 1.12 established compiler performance baselines that later versions improved upon but never regressed below.
Config.Reader - Runtime Configuration
The new Config.Reader module separates configuration reading from configuration evaluation. Previous Elixir versions mixed these concerns through runtime.exs files that executed during application startup. The separation enables more predictable configuration behavior in production systems.
# Read configuration from file
config = Config.Reader.read!("config/runtime.exs")
# => Returns keyword list: [
# => app_name: [
# => port: 4000, # => Integer value
# => database_url: "postgres://..." # => String value
# => ]
# => ]
# Merge configurations from multiple sources
base_config = Config.Reader.read!("config/runtime.exs")
# => Base configuration: [database: [pool_size: 10]]
override_config = Config.Reader.read!("config/production.exs")
# => Override configuration: [database: [pool_size: 20]]
final_config = Config.Reader.merge(base_config, override_config)
# => Merged result: [database: [pool_size: 20]]
# => Override values replace base values for matching keysConfiguration readers support validation before deployment. A deployment script can read configuration files, validate required keys exist, and fail fast if configuration is malformed. This prevents runtime failures from misconfiguration.
Scripted Mix Install
Mix.install allows standalone Elixir scripts to declare dependencies inline. The script downloads dependencies, compiles them, and runs the code - all from a single file execution. This eliminates the need for separate mix.exs project files for simple automation tasks.
# deployment_validator.exs - Standalone deployment validation script
Mix.install([
{:jason, "~> 1.4"} # => Declares Jason JSON library dependency
# => Mix downloads and compiles if not cached
])
# Script uses installed dependencies immediately
config_json = File.read!("config/deploy.json")
# => Reads JSON file: "{\"environment\": \"production\", \"replicas\": 3}"
config = Jason.decode!(config_json)
# => Parses JSON using installed Jason library
# => Returns map: %{"environment" => "production", "replicas" => 3}
IO.puts("Deploying to #{config["environment"]} with #{config["replicas"]} replicas")
# => Output: Deploying to production with 3 replicasFinancial system deployment scripts benefit from this feature. A revenue calculation validation script can install the Decimal library, read transaction data, verify calculations match expected totals, and output a compliance report - all from a single executable file. Operations teams run the script without maintaining separate project dependencies.
# revenue_validator.exs - Standalone revenue calculation validator
Mix.install([
{:decimal, "~> 2.0"}, # => Precise decimal arithmetic
{:jason, "~> 1.4"} # => JSON parsing
])
# Read transaction data from JSON export
transactions = "transactions.json"
|> File.read!()
# => Raw JSON string from file
|> Jason.decode!()
# => Parsed list of transaction maps
# Calculate revenue totals with precision
total_revenue = Enum.reduce(transactions, Decimal.new(0), fn tx, acc ->
# => tx: %{"amount" => "125.50", "currency" => "USD"}
# => acc starts at Decimal 0.00
amount = Decimal.new(tx["amount"])
# => Converts string "125.50" to Decimal (no floating-point errors)
Decimal.add(acc, amount)
# => Adds to accumulator: 0.00 + 125.50 = 125.50
# => Next iteration: 125.50 + 89.75 = 215.25
end)
# => Final total_revenue: Decimal representing exact sum
IO.puts("Total Revenue: #{Decimal.to_string(total_revenue)}")
# => Output: Total Revenue: 45892.75
# => No rounding errors from floating-point arithmeticThe caching behavior improves script startup time. Mix.install downloads dependencies once and caches them. Subsequent script executions skip the download phase and run immediately. CI/CD pipelines cache these dependencies the same way they cache mix dependencies for projects.
Compilation Improvements
The compiler gained performance optimizations that reduce build times for large projects. Projects with hundreds of modules see compilation time reductions of 10-15% compared to Elixir 1.11. The improvements come from better dependency tracking between modules.
# Module dependency tracking example
defmodule PaymentProcessor do
# => Defines payment processing module
alias InvoiceValidator
# => Declares dependency on InvoiceValidator module
# => Compiler tracks this relationship
def process(payment) do
# => payment: %{invoice_id: "INV-001", amount: 1250.00}
case InvoiceValidator.validate(payment.invoice_id) do
# => Calls InvoiceValidator.validate/1
# => Returns {:ok, invoice} or {:error, reason}
{:ok, invoice} ->
# => invoice: %{id: "INV-001", status: :pending}
charge_account(payment, invoice)
# => Proceeds with payment processing
{:error, reason} ->
# => reason: :invoice_not_found or :already_paid
{:error, "Invalid invoice: #{reason}"}
# => Returns error without processing payment
end
end
endWhen InvoiceValidator changes, the compiler recompiles PaymentProcessor because it depends on the validator. However, if an unrelated module like EmailSender changes, PaymentProcessor skips recompilation. This selective recompilation reduces wasted build time.
ExUnit Concurrency
ExUnit test runner improvements reduce test suite execution time through better parallel execution. Tests tagged with async: true run concurrently with better CPU utilization. A test suite with 1000 async tests completes 20-30% faster in Elixir 1.12 compared to 1.11.
defmodule PaymentProcessorTest do
use ExUnit.Case, async: true
# => async: true enables parallel test execution
# => Safe because tests don't share state
test "processes valid payment" do
# => Test runs in isolated process
payment = %{invoice_id: "INV-001", amount: 1250.00}
# => payment: %{invoice_id: "INV-001", amount: 1250.00}
result = PaymentProcessor.process(payment)
# => Calls function under test
# => result: {:ok, %{status: :completed, transaction_id: "TXN-123"}}
assert {:ok, %{status: :completed}} = result
# => Pattern matches on success case
# => Test passes if payment completed successfully
end
test "rejects invalid invoice" do
# => Runs in parallel with other async tests
# => Different process from previous test
payment = %{invoice_id: "INVALID", amount: 100.00}
# => payment with non-existent invoice ID
result = PaymentProcessor.process(payment)
# => result: {:error, "Invalid invoice: invoice_not_found"}
assert {:error, _reason} = result
# => Pattern matches on error case
# => Test passes if payment rejected as expected
end
endThe test scheduler distributes tests across CPU cores more evenly. Previously, some cores finished early while others processed long-running tests. The 1.12 scheduler balances work better, keeping all cores busy until test completion.
Why This Is Baseline
OSE Platform designates Elixir 1.12 as the baseline version for three strategic reasons. First, it represents the oldest version the platform supports - all code must run on 1.12 or newer. Second, it establishes the minimum feature set available to developers. Third, it defines compatibility boundaries for library dependencies.
The stability argument for choosing 1.12 rests on production evidence. Thousands of organizations ran 1.12 in production for years without critical bugs. The Elixir core team fixed all reported issues through patch releases (1.12.1, 1.12.2, 1.12.3). By the time 1.13 released, 1.12 had achieved mature stability.
Backward compatibility concerns influenced the baseline choice. Code written for 1.12 runs on all later versions (1.13, 1.14, 1.15, 1.16, 1.17) without modification. This guarantee allows OSE Platform to upgrade Elixir versions without breaking existing applications. The platform can safely recommend users stay current with the latest stable Elixir release.
The feature completeness of 1.12 meets OSE Platform requirements. The release includes all essential features for building production systems: supervision trees, GenServers, robust error handling, and the BEAM VM’s fault tolerance. Later releases add conveniences (better error messages, type hints, performance improvements) but don’t fundamentally change how applications are structured.
Upgrade Guidance
Users running Elixir versions older than 1.12 face a mandatory upgrade to use OSE Platform. The upgrade process from 1.11 to 1.12 involves minimal breaking changes - most code upgrades without modification.
# Upgrade checklist for Elixir 1.11 → 1.12
# 1. Update mix.exs elixir version requirement
def project do
[
app: :financial_system,
version: "1.0.0",
elixir: "~> 1.12", # => Updated from "~> 1.11"
# => Accepts 1.12.0 through 1.12.x
deps: deps()
]
end
# 2. Replace deprecated config functions
# OLD (1.11 approach):
# Config.config_env() # => Returns :dev, :test, or :prod
# NEW (1.12 approach):
runtime_env = Application.get_env(:my_app, :environment)
# => Reads environment from application config
# => More explicit, less reliance on compile-time values
# 3. Update Mix.install scripts if used
Mix.install([
{:jason, "~> 1.4"}
], force: true) # => force option available in 1.12
# => Reinstalls dependencies even if cached
# => Useful for debugging dependency issues
# 4. Test async ExUnit tests for new concurrency behavior
# No code changes needed, but verify test isolation
# Tests may run in different order due to improved schedulingThe upgrade from 1.11 to 1.12 rarely breaks production code. Most teams complete the upgrade in hours rather than days. Comprehensive test suites catch the few edge cases that need adjustment. The Elixir changelog documents all changes clearly.
For teams starting fresh today, begin with Elixir 1.17 or later rather than 1.12. OSE Platform supports 1.12 as the minimum version, but newer releases include significant improvements (better error messages, type system foundations, performance optimizations). Use 1.12 compatibility only when maintaining legacy systems or ensuring broad deployment compatibility.
Organizations maintaining existing 1.12 deployments can stay on this version safely. The release remains stable and receives security patches if needed. However, planning an upgrade path to 1.14+ brings benefits: improved developer experience through better error messages, performance gains from compiler optimizations, and access to the gradual type system for catching bugs earlier.
The upgrade path from 1.12 to 1.17 should proceed incrementally: 1.12 → 1.14 → 1.17. Each intermediate version validates one set of changes rather than jumping across multiple major feature additions. Test thoroughly at each step, addressing deprecation warnings before proceeding to the next version. This approach minimizes risk while capturing the benefits of newer releases.