Skip to content
Crystal: Ruby-Inspired Compiled Language

Crystal: Ruby-Inspired Compiled Language

DodaTech Updated Jun 20, 2026 6 min read

Crystal is a compiled language with Ruby-like syntax, global type inference, and native performance via LLVM. It feels like Ruby — readable, expressive, developer-friendly — but compiles to a single static binary that runs as fast as C code.

In this tutorial, you’ll learn Crystal’s Ruby-inspired syntax, type inference, concurrency with fibers, C bindings, popular web frameworks (Amber, Lucky), and how Crystal compiles to native executables.

What You’ll Learn

  • Ruby-like syntax with static typing
  • Global type inference — no type annotations needed
  • Compilation to native binary via LLVM
  • Concurrency with fibers and channels
  • C bindings without a FFI library
  • Crystal web frameworks: Amber, Lucky, Kemal

Why Crystal Matters

Crystal targets the same sweet spot as Go: the performance of a compiled language with the productivity of a dynamic one. It’s used for web services, CLI tools, and high-performance APIs. At DodaTech, Crystal’s type inference and binary compilation make it ideal for DodaZIP’s command-line tools.

Learning Path

    flowchart LR
  A[Ruby Basics] --> B[Crystal Basics<br/>You are here]
  B --> C[Type Inference]
  C --> D[Fibers & Concurrency]
  D --> E[Crystal Web Apps]
  style B fill:#f90,color:#fff
  

Crystal Syntax

If you know Ruby, you already know Crystal syntax:

# hello.cr
puts "Hello, Crystal!"

# Variables — type inferred
name = "Alice"
age = 30
height = 1.75

# String interpolation
puts "Name: #{name}, Age: #{age}"

# Everything is an object
puts "hello".upcase      # HELLO
puts 42.to_s             # "42"

# Methods
def greet(name : String)
  "Hello, #{name}!"
end

puts greet("Bob")  # Hello, Bob!

# Blocks
3.times { |i| puts "Iteration #{i}" }
# Iteration 0
# Iteration 1
# Iteration 2

Expected output: The Crystal syntax is nearly identical to Ruby. Key difference: Crystal requires type annotations for method parameters in public APIs.

Type Inference

Crystal infers types globally — no annotations needed for local variables:

# Type inference — Crystal tracks every type
def process(x)
  if x.is_a?(Int32)
    x * 2          # Infers: Int32
  elsif x.is_a?(String)
    x.upcase       # Infers: String
  end
end

puts process(42)       # 84
puts process("hello")  # HELLO

# Union types — variable can be one of several types
value = rand > 0.5 ? "text" : 42
# value is Int32 | String (union type)
puts typeof(value)  # Int32 | String

# Case expression type narrowing
case value
when String
  puts value.upcase   # Crystal knows it's a String here
when Int32
  puts value * 2      # Crystal knows it's an Int32 here
end

Expected behavior: process returns either Int32 or String. Crystal tracks union types through all code paths. Accessing a union type requires type narrowing via is_a? or case.

Compilation to Native Binary

Compile and run in one step:

crystal run hello.cr    # Compile + run (cached)
crystal build hello.cr  # Produce static binary
./hello                 # 1.2MB binary, immediate startup
# Build a production binary
# crystal build --release hello.cr
# Stripped binary: ~300KB for a simple program
puts "Hello from a native binary!"

Expected behavior: crystal run compiles and executes. crystal build produces a standalone binary with zero dependencies.

Concurrency with Fibers

Crystal fibers are lightweight concurrency primitives — like goroutines but with channels:

# Spawn fibers and communicate via channels
channel = Channel(String).new

# Fiber 1
spawn do
  sleep 0.5.seconds
  channel.send("Hello from fiber 1")
end

# Fiber 2
spawn do
  sleep 0.3.seconds
  channel.send("Hello from fiber 2")
end

# Receive in order they complete
2.times do
  msg = channel.receive
  puts msg
end

Expected output: Hello from fiber 2 (0.3s) then Hello from fiber 1 (0.5s). The second fiber finishes first despite being spawned second.

C Bindings

Call C libraries directly with Crystal’s lib syntax:

# Bind to libc's printf
lib LibC
  fun printf(format : UInt8*, ...) : Int32
end

LibC.printf("Hello from C: %d\n", 42)

# Bind to SQLite
@[Link("sqlite3")]
lib SQLite3
  fun open(path : UInt8*, db : Void**) : Int32
  fun close(db : Void*) : Int32
end

db = Pointer(Void).malloc(1)
SQLite3.open("test.db", pointerof(db))
puts "Database opened!"
SQLite3.close(db)

Expected behavior: Crystal links directly to shared libraries. No FFI wrapper needed — the lib block generates bindings automatically.

Web Frameworks

Crystal has several web frameworks:

FrameworkStyleFeatures
KemalSinatra-likeSimple routing, middleware, WebSocket
AmberRails-likeFull-stack, ORM, templates, WebSocket
LuckyType-safeCompile-time checks, strong typing
AthenaDI-basedDependency injection, testing
# Kemal — minimal web server
require "kemal"

get "/" do
  "Hello, Crystal!"
end

get "/hello/:name" do |env|
  name = env.params.url["name"]
  "Hello, #{name}!"
end

Kemal.run

Expected behavior: crystal run starts a web server on port 3000. Visiting http://localhost:3000/ shows “Hello, Crystal!”. Visiting /hello/Alice shows “Hello, Alice!”.

Common Mistakes

1. Not Using Type Annotations for Public Methods

Crystal requires type annotations for public method parameters. Private methods can infer types from their callers.

2. Forgetting That Nil Is a Type

String? means String | Nil. You must check for nil before using the value. Crystal won’t compile if you don’t.

3. Confusing spawn with fork

spawn creates a fiber (lightweight, cooperative). It does NOT create an OS thread or process.

4. Blocking the Event Loop

Crystal uses an event loop for I/O. Long synchronous operations block all fibers. Use spawn for CPU-heavy work.

5. Not Using --release for Production

Debug builds are slower and larger. Always compile with --release for production deployment.

6. Ignoring Crystal’s Formatter

Run crystal tool format to auto-format your code. Crystal’s style is enforced, not debated.

Practice Questions

1. How does Crystal achieve type safety without type annotations?

Crystal uses global type inference. It traces all code paths and infers types at compile time.

2. What is a fiber in Crystal?

A lightweight unit of concurrency. Fibers are cooperative (not preemptive) and multiplexed onto OS threads.

3. How do you call a C function from Crystal?

Use a lib block to declare the function signature. Crystal generates bindings automatically.

4. What’s the difference between Kemal and Amber?

Kemal is minimal (like Sinatra). Amber is full-stack (like Rails) with ORM, authentication, and CLI generators.

5. Challenge: Build a concurrent URL checker.

Spawn fibers to check multiple URLs concurrently. Print each URL’s HTTP status code. Use a Channel to collect results.

Mini Project: HTTP Status Checker

require "http/client"

def check_url(url : String, channel : Channel(String))
  response = HTTP::Client.get(url)
  channel.send("#{url}: #{response.status_code}")
end

urls = ["https://crystal-lang.org", "https://github.com", "https://google.com"]
channel = Channel(String).new

urls.each { |url| spawn { check_url(url, channel) } }
urls.size.times { puts channel.receive }

FAQ

Is Crystal compatible with Ruby gems?
No. Crystal compiles to native code and has its own package manager (Shards). The syntax is Ruby-like but the runtime is completely different.
How does Crystal manage memory?
Crystal uses garbage collection (Boehm GC by default). For real-time systems, you can use --no-gc and manage memory manually.
Is Crystal production-ready?
Yes. Companies use Crystal in production for web services, APIs, and CLI tools. The ecosystem is growing but smaller than Ruby’s.

What’s Next

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro