Agents convert-elixir-scala
Bidirectional conversion between Elixir and Scala. Use when migrating projects between these languages in either direction. Extends meta-convert-dev with Elixir↔Scala specific patterns. Use when migrating Elixir/Phoenix applications to Scala, translating BEAM OTP patterns to Akka actors, or refactoring dynamic concurrency to statically-typed JVM patterns. Extends meta-convert-dev with Elixir-to-Scala specific patterns.
git clone https://github.com/aRustyDev/agents
T=$(mktemp -d) && git clone --depth=1 https://github.com/aRustyDev/agents "$T" && mkdir -p ~/.claude/skills && cp -r "$T/content/skills/convert-elixir-scala" ~/.claude/skills/arustydev-agents-convert-elixir-scala && rm -rf "$T"
content/skills/convert-elixir-scala/SKILL.mdElixir ↔ Scala Conversion
Bidirectional conversion between Elixir and Scala. This skill extends
meta-convert-dev with Elixir↔Scala specific type mappings, idiom translations, and tooling for translating between BEAM VM's dynamic actors and JVM's statically-typed Akka actors.
This Skill Extends
- Foundational conversion patterns (APTV workflow, testing strategies)meta-convert-dev
For general concepts like the Analyze → Plan → Transform → Validate workflow, testing strategies, and common pitfalls, see the meta-skill first.
This Skill Adds
- Type mappings: Elixir types (dynamic) → Scala types (static)
- Idiom translations: Elixir patterns → idiomatic Scala with type safety
- Runtime transition: BEAM VM (lightweight processes) → JVM (Akka actors/Futures)
- Concurrency models: GenServer/Supervisor → Akka actors with typed protocols
- OTP patterns: Supervision trees → Akka supervision hierarchies
- Data structures: Elixir maps/structs → Scala case classes
- Error handling: {:ok/:error} tuples → Either/Try monads
- Build tools: Mix → sbt/Mill
This Skill Does NOT Cover
- General conversion methodology - see
meta-convert-dev - Elixir language fundamentals - see
lang-elixir-dev - Scala language fundamentals - see
lang-scala-dev - Phoenix framework specifics - see
for web frameworkslang-scala-play-dev
Quick Reference
| Elixir | Scala | Notes |
|---|---|---|
| Symbol / sealed trait | Atoms → sealed traits for exhaustiveness |
| | Result tuples → Either/Try |
| | Error tuples → Either |
| | Maps require type parameters |
| | Lists immutable by default |
| | Module → singleton object |
| | Functions require type signatures |
| | Anonymous functions |
(pipe) | | Pipe → method chaining |
| | Typed actors in Akka |
| / | Supervision strategies |
| | Async execution |
| Pattern matching | | Similar but with static types |
8 Pillars Validation
Elixir Coverage
| Pillar | Coverage | Status |
|---|---|---|
| Module System | ✓ | defmodule, alias, import, use |
| Error Handling | ✓ | {:ok/:error} tuples, pattern matching, try/rescue |
| Concurrency Model | ✓ | Processes, GenServer, Supervisor, Task |
| Metaprogramming | ✓ | Macros, use directive, compile-time generation |
| Zero/Default Values | ✓ | nil, pattern matching with defaults |
| Serialization | ✓ | Phoenix params, Jason/Poison, Ecto |
| Build/Deps | ✓ | Mix, hex.pm, deps management |
| Testing Idioms | ✓ | ExUnit, doctests, Mox for mocking |
Score: 8/8 (Green)
Scala Coverage
| Pillar | Coverage | Status |
|---|---|---|
| Module System | ✓ | Packages, objects, imports, traits |
| Error Handling | ✓ | Either, Try, Option, exceptions |
| Concurrency Model | ✓ | Futures, Akka actors, Cats Effect IO |
| Metaprogramming | ✓ | Macros (Scala 2), inline/metaprogramming (Scala 3) |
| Zero/Default Values | ✓ | Option, default parameters, null (avoid) |
| Serialization | ✓ | Play JSON, Circe, uPickle, case classes |
| Build/Deps | ✓ | sbt, Mill, Maven Central |
| Testing Idioms | ✓ | ScalaTest, ScalaCheck, MUnit, specs2 |
Score: 8/8 (Green)
Validation Status: ✅ Both languages have comprehensive coverage. Conversion can proceed with confidence.
Key Challenge: Elixir's dynamic typing to Scala's static typing requires careful type inference and explicit type annotations.
Type System Mapping
Primitive Types
| Elixir | Scala | Notes |
|---|---|---|
| / sealed trait | Use sealed traits for ADTs |
| / / | Choose based on range needs |
| | 64-bit floating point |
/ | | Direct mapping |
| / (avoid) | Prefer Option[T] |
| | Immutable strings |
| | Less common in Scala |
Collection Types
| Elixir | Scala | Notes |
|---|---|---|
| | Immutable linked lists |
| | Tagged tuples → Either |
| | Tuples map directly |
| | Immutable maps |
| | Immutable sets |
| Keyword list | | Less common in Scala |
Range | | Inclusive → exclusive end |
Composite Types
| Elixir | Scala | Notes |
|---|---|---|
| | Structs → case classes |
| | Constructor syntax |
| | Type aliases |
| | Function signatures required |
| Protocol | Trait | Polymorphism via traits |
Idiom Translation
Pattern: Module Definition
Elixir:
defmodule MyApp.Users do @moduledoc """ Handles user-related operations. """ alias MyApp.Repo import Ecto.Query @type user :: %{id: integer, name: String.t()} @spec get_user(integer) :: {:ok, user} | {:error, :not_found} def get_user(id) do case Repo.get(User, id) do nil -> {:error, :not_found} user -> {:ok, user} end end defp build_query(filters) do from u in User, where: ^filters end end
Scala:
package myapp /** * Handles user-related operations. */ object Users { import myapp.repo.Repo import myapp.models.User type UserId = Int type UserResult = Either[UserError, User] sealed trait UserError case object NotFound extends UserError def getUser(id: UserId): UserResult = { Repo.get[User](id) match { case Some(user) => Right(user) case None => Left(NotFound) } } private def buildQuery(filters: Map[String, Any]): Query = { Query.from[User].where(filters) } }
Why this translation:
→defmodule
(singleton)object
→ Scaladoc@moduledoc/** */
→@type
alias with explicit typestype
→ method signature with types@spec
tuples →{:ok/:error}Either[Error, Value]- Pattern matching on nil →
Option.match - CamelCase modules → package + object structure
Pattern: Pattern Matching to Case Classes
Elixir:
def process_response({:ok, %{"data" => data, "meta" => meta}}) do {:success, data, meta} end def process_response({:error, reason}) do {:failure, reason} end def handle_user(%User{name: name, age: age}) when age >= 18 do "Adult: #{name}" end
Scala:
sealed trait Response case class Success(data: String, meta: Map[String, Any]) extends Response case class Error(reason: String) extends Response def processResponse(response: Response): Result = response match { case Success(data, meta) => SuccessResult(data, meta) case Error(reason) => FailureResult(reason) } case class User(name: String, age: Int) def handleUser(user: User): Option[String] = user match { case User(name, age) if age >= 18 => Some(s"Adult: $name") case _ => None }
Why this translation:
- Multiple function clauses → sealed trait with case classes
- Guards → pattern guards with
in matchif - Struct pattern matching → case class pattern matching
- Tag tuples → sealed trait hierarchy for type safety
- Exhaustiveness checking enforced by compiler
Pattern: Pipelines to Method Chaining
Elixir:
def process_data(input) do input |> String.trim() |> String.downcase() |> String.split(",") |> Enum.map(&String.trim/1) |> Enum.reject(&(&1 == "")) |> Enum.join(";") end
Scala:
def processData(input: String): String = { input .trim .toLowerCase .split(",") .map(_.trim) .filterNot(_.isEmpty) .mkString(";") } // Or with explicit types for clarity def processData(input: String): String = { val trimmed: String = input.trim val lowercased: String = trimmed.toLowerCase val parts: Array[String] = lowercased.split(",") val cleaned: Array[String] = parts.map(_.trim).filterNot(_.isEmpty) cleaned.mkString(";") }
Why this translation:
→ method chaining (|>
).- Enum functions → collection methods
- Capture operator
→ anonymous functions&func/arity_
→Enum.rejectfilterNot
→Enum.joinmkString
Pattern: GenServer to Akka Typed Actor
Elixir:
defmodule UserCache do use GenServer # Client API def start_link(opts) do GenServer.start_link(__MODULE__, opts, name: __MODULE__) end def get(key) do GenServer.call(__MODULE__, {:get, key}) end def put(key, value) do GenServer.cast(__MODULE__, {:put, key, value}) end # Server Callbacks @impl true def init(_opts) do {:ok, %{}} end @impl true def handle_call({:get, key}, _from, state) do {:reply, Map.get(state, key), state} end @impl true def handle_cast({:put, key, value}, state) do {:noreply, Map.put(state, key, value)} end end
Scala (Akka Typed):
import akka.actor.typed._ import akka.actor.typed.scaladsl.Behaviors object UserCache { // Protocol sealed trait Command final case class Get(key: String, replyTo: ActorRef[Option[String]]) extends Command final case class Put(key: String, value: String) extends Command // Behavior def apply(): Behavior[Command] = { behavior(Map.empty[String, String]) } private def behavior(cache: Map[String, String]): Behavior[Command] = { Behaviors.receive { (context, message) => message match { case Get(key, replyTo) => replyTo ! cache.get(key) Behaviors.same case Put(key, value) => behavior(cache + (key -> value)) } } } } // Usage val system: ActorSystem[UserCache.Command] = ActorSystem(UserCache(), "user-cache-system") implicit val timeout: Timeout = 3.seconds val futureResult: Future[Option[String]] = system.ask(ref => UserCache.Get("key", ref))
Why this translation:
→ Akka TypedGenServerActor[Protocol]- Message tuples → sealed trait protocol
→ synchronous message handling withhandle_call
patternask
→ asynchronous message handlinghandle_cast- State recursion → immutable state updates
- Type safety enforced at compile time
Pattern: Supervisor to Akka Supervision
Elixir:
defmodule MyApp.Application do use Application @impl true def start(_type, _args) do children = [ {UserCache, []}, {MyWorker, name: MyWorker, restart: :transient}, {Registry, keys: :unique, name: MyApp.Registry} ] opts = [strategy: :one_for_one, name: MyApp.Supervisor] Supervisor.start_link(children, opts) end end
Scala (Akka Typed):
import akka.actor.typed._ import akka.actor.typed.scaladsl._ object MyApp { sealed trait RootCommand def apply(): Behavior[RootCommand] = { Behaviors.setup { context => // Spawn supervised children val userCache = context.spawn( UserCache(), "user-cache" ) val worker = context.spawn( Behaviors.supervise(MyWorker()) .onFailure[Exception](SupervisorStrategy.restart), "my-worker" ) // Root behavior Behaviors.receiveMessage { message => Behaviors.same } } } } // Starting the system val system = ActorSystem(MyApp(), "my-app-system")
Why this translation:
- Supervision tree →
with spawned childrenBehaviors.setup
→ individual supervision per actor:one_for_one
restart →:transientSupervisorStrategy.restart.withLimit- Named processes → named actors via
spawn - Declarative children list → imperative spawn calls
Error Handling
Elixir Error Model → Scala Error Model
Elixir uses tagged tuples:
def divide(a, b) when b != 0, do: {:ok, a / b} def divide(_, 0), do: {:error, :division_by_zero} # Usage case divide(10, 2) do {:ok, result} -> "Result: #{result}" {:error, reason} -> "Error: #{reason}" end # Or with pattern matching {:ok, result} = divide(10, 2)
Scala uses Either/Try monads:
sealed trait MathError case object DivisionByZero extends MathError def divide(a: Double, b: Double): Either[MathError, Double] = { if (b != 0) Right(a / b) else Left(DivisionByZero) } // Usage with pattern matching divide(10, 2) match { case Right(result) => s"Result: $result" case Left(error) => s"Error: $error" } // Or with for-comprehension val result = for { x <- divide(10, 2) y <- divide(x, 5) } yield y // Or with Try for exceptions import scala.util.{Try, Success, Failure} def divideWithTry(a: Double, b: Double): Try[Double] = { Try(a / b) } divideWithTry(10, 0) match { case Success(value) => s"Result: $value" case Failure(exception) => s"Error: ${exception.getMessage}" }
Translation strategy:
→{:ok, value}Right(value): Either[Error, Value]
→{:error, reason}Left(reason): Either[Error, Value]- Define error ADT with sealed trait
- Use for-comprehensions for chaining operations
- Consider
for exception-based codeTry
Concurrency Patterns
Elixir Async → Scala Async
Elixir Task-based async:
def fetch_user_data(user_id) do task1 = Task.async(fn -> fetch_profile(user_id) end) task2 = Task.async(fn -> fetch_orders(user_id) end) task3 = Task.async(fn -> fetch_preferences(user_id) end) profile = Task.await(task1) orders = Task.await(task2) preferences = Task.await(task3) %{profile: profile, orders: orders, preferences: preferences} end
Scala Future-based async:
import scala.concurrent.{Future, Await} import scala.concurrent.duration._ import scala.concurrent.ExecutionContext.Implicits.global def fetchUserData(userId: Int): Future[UserData] = { val profileF = Future { fetchProfile(userId) } val ordersF = Future { fetchOrders(userId) } val preferencesF = Future { fetchPreferences(userId) } for { profile <- profileF orders <- ordersF preferences <- preferencesF } yield UserData(profile, orders, preferences) } // Or with Applicative for parallel execution import cats.implicits._ def fetchUserDataParallel(userId: Int): Future[UserData] = { ( Future { fetchProfile(userId) }, Future { fetchOrders(userId) }, Future { fetchPreferences(userId) } ).mapN(UserData.apply) }
Why this translation:
→Task.asyncFuture { }
→Task.await
(avoid) or for-comprehensionAwait.result- Parallel execution → spawn all Futures immediately
- Sequential composition → for-comprehension
- Use Cats for applicative parallel composition
Process Message Passing → Actor Tell/Ask
Elixir:
defmodule Counter do def start do spawn(fn -> loop(0) end) end defp loop(count) do receive do {:increment, caller} -> send(caller, {:ok, count + 1}) loop(count + 1) {:get, caller} -> send(caller, {:ok, count}) loop(count) :stop -> :ok end end end # Usage counter = Counter.start() send(counter, {:increment, self()}) receive do {:ok, new_value} -> IO.puts("New value: #{new_value}") end
Scala (Akka Typed):
object Counter { sealed trait Command case class Increment(replyTo: ActorRef[Response]) extends Command case class Get(replyTo: ActorRef[Response]) extends Command case object Stop extends Command sealed trait Response case class Value(count: Int) extends Response def apply(initialCount: Int): Behavior[Command] = { counter(initialCount) } private def counter(count: Int): Behavior[Command] = { Behaviors.receive { (context, message) => message match { case Increment(replyTo) => replyTo ! Value(count + 1) counter(count + 1) case Get(replyTo) => replyTo ! Value(count) Behaviors.same case Stop => Behaviors.stopped } } } } // Usage val system = ActorSystem(Counter(0), "counter-system") implicit val timeout: Timeout = 3.seconds val futureCount: Future[Counter.Value] = system.ask(ref => Counter.Get(ref))
Why this translation:
→spawn
/ActorSystemcontext.spawn
→ actorsend
(tell) operator!
→receive
pattern matchingBehaviors.receive- Message tuples → sealed trait protocol
- Tail recursion → return new behavior
Common Pitfalls
1. Dynamic Typing → Static Typing
Problem: Elixir allows dynamic typing; Scala requires explicit types.
# Elixir - dynamic def process(data) do case data do x when is_integer(x) -> x * 2 x when is_binary(x) -> String.length(x) x when is_list(x) -> length(x) end end
Solution: Use sealed trait ADT for type safety.
sealed trait Data case class IntData(value: Int) extends Data case class StringData(value: String) extends Data case class ListData[A](values: List[A]) extends Data def process(data: Data): Int = data match { case IntData(x) => x * 2 case StringData(x) => x.length case ListData(xs) => xs.length }
2. Pattern Match Exhaustiveness
Problem: Elixir warns on non-exhaustive matches; Scala errors.
# Elixir - warning only def handle({:ok, value}), do: value # Missing {:error, _} case
Solution: Always handle all cases or use sealed traits.
sealed trait Result[+A] case class Success[A](value: A) extends Result[A] case class Failure(error: String) extends Result[Nothing] def handle[A](result: Result[A]): A = result match { case Success(value) => value case Failure(error) => throw new Exception(error) // Compiler enforces exhaustiveness }
3. Nil Safety
Problem: Elixir has
nil; Scala should use Option.
# Elixir user = Repo.get(User, id) # Can be nil name = user.name # Runtime error if nil
Solution: Use Option type.
val user: Option[User] = Repo.get[User](id) val name: Option[String] = user.map(_.name) // Or with for-comprehension val name = for { u <- user } yield u.name
4. Immutability by Convention → Enforced
Problem: Elixir immutability is by convention; Scala enforces it.
# Elixir - creates new map map = %{a: 1} new_map = Map.put(map, :b, 2)
Solution: Use
val and immutable collections.
val map = Map("a" -> 1) val newMap = map + ("b" -> 2) // Creates new map // Avoid var and mutable collections var mutableMap = scala.collection.mutable.Map("a" -> 1) // Avoid
5. Process Supervision → Actor Supervision
Problem: Different restart semantics and error handling.
Elixir:
# Let it crash - supervisor restarts def handle_call(:dangerous, _from, state) do result = do_dangerous_operation() # Might crash {:reply, result, state} end
Scala:
// Must handle errors explicitly or define supervision strategy Behaviors.supervise { Behaviors.receiveMessage { case Dangerous => try { val result = doDangerousOperation() Behaviors.same } catch { case e: Exception => throw e // Supervisor handles via strategy } } }.onFailure[Exception](SupervisorStrategy.restart)
Tooling
| Category | Elixir | Scala | Notes |
|---|---|---|---|
| Build Tool | Mix | sbt, Mill | Mill is more modern |
| Package Manager | Hex | Maven Central | sbt resolves dependencies |
| REPL | iex | scala / ammonite | Ammonite more feature-rich |
| Formatter | mix format | scalafmt | Both auto-format |
| Linter | Credo | scalafix, Wartremover | Scala has multiple |
| Testing | ExUnit | ScalaTest, MUnit | Multiple test frameworks |
| Property Testing | StreamData | ScalaCheck | Similar concepts |
| Actor System | OTP | Akka | Akka Typed recommended |
| Web Framework | Phoenix | Play, Http4s, Akka HTTP | Play most similar |
| DB | Ecto | Slick, Doobie, Quill | Different approaches |
| JSON | Jason, Poison | Circe, Play JSON, uPickle | Macro-based derivation |
| HTTP Client | HTTPoison, Tesla | sttp, http4s-client | Functional HTTP clients |
Examples
Example 1: Simple - Data Transformation
Before (Elixir):
defmodule DataProcessor do def transform_user(%{name: name, age: age}) do %{ display_name: String.upcase(name), is_adult: age >= 18, category: categorize_age(age) } end defp categorize_age(age) when age < 13, do: :child defp categorize_age(age) when age < 20, do: :teenager defp categorize_age(_), do: :adult end
After (Scala):
object DataProcessor { case class User(name: String, age: Int) sealed trait AgeCategory case object Child extends AgeCategory case object Teenager extends AgeCategory case object Adult extends AgeCategory case class TransformedUser( displayName: String, isAdult: Boolean, category: AgeCategory ) def transformUser(user: User): TransformedUser = { TransformedUser( displayName = user.name.toUpperCase, isAdult = user.age >= 18, category = categorizeAge(user.age) ) } private def categorizeAge(age: Int): AgeCategory = age match { case a if a < 13 => Child case a if a < 20 => Teenager case _ => Adult } }
Example 2: Medium - Error Handling Pipeline
Before (Elixir):
defmodule UserRegistration do def register(params) do with {:ok, validated} <- validate_params(params), {:ok, user} <- create_user(validated), {:ok, _email} <- send_welcome_email(user) do {:ok, user} else {:error, :invalid_email} -> {:error, "Email is invalid"} {:error, :user_exists} -> {:error, "User already exists"} {:error, reason} -> {:error, "Registration failed: #{reason}"} end end defp validate_params(%{email: email, password: password}) when byte_size(password) >= 8 do if valid_email?(email) do {:ok, %{email: email, password: password}} else {:error, :invalid_email} end end defp validate_params(_), do: {:error, :invalid_params} end
After (Scala):
object UserRegistration { case class UserParams(email: String, password: String) case class User(id: Int, email: String) sealed trait RegistrationError case object InvalidEmail extends RegistrationError case object UserExists extends RegistrationError case class GeneralError(message: String) extends RegistrationError type RegistrationResult = Either[RegistrationError, User] def register(params: UserParams): RegistrationResult = { for { validated <- validateParams(params) user <- createUser(validated) _ <- sendWelcomeEmail(user) } yield user } private def validateParams( params: UserParams ): Either[RegistrationError, UserParams] = { if (params.password.length < 8) { Left(GeneralError("Password too short")) } else if (!validEmail(params.email)) { Left(InvalidEmail) } else { Right(params) } } private def createUser( params: UserParams ): Either[RegistrationError, User] = { // Database logic Right(User(1, params.email)) } private def sendWelcomeEmail( user: User ): Either[RegistrationError, Unit] = { // Email logic Right(()) } private def validEmail(email: String): Boolean = { email.contains("@") } }
Example 3: Complex - GenServer to Akka Actor with State
Before (Elixir):
defmodule RateLimiter do use GenServer require Logger defstruct requests: %{}, window_ms: 60_000, max_requests: 100 # Client API def start_link(opts) do GenServer.start_link(__MODULE__, opts, name: __MODULE__) end def check_rate(user_id) do GenServer.call(__MODULE__, {:check_rate, user_id}) end def reset(user_id) do GenServer.cast(__MODULE__, {:reset, user_id}) end # Server Callbacks @impl true def init(opts) do state = %__MODULE__{ window_ms: Keyword.get(opts, :window_ms, 60_000), max_requests: Keyword.get(opts, :max_requests, 100) } {:ok, state} end @impl true def handle_call({:check_rate, user_id}, _from, state) do now = System.system_time(:millisecond) requests = Map.get(state.requests, user_id, []) # Filter out expired requests valid_requests = Enum.filter(requests, fn ts -> now - ts < state.window_ms end) count = length(valid_requests) if count < state.max_requests do new_requests = [now | valid_requests] new_state = put_in(state.requests[user_id], new_requests) {:reply, {:ok, state.max_requests - count - 1}, new_state} else {:reply, {:error, :rate_limit_exceeded}, state} end end @impl true def handle_cast({:reset, user_id}, state) do new_state = put_in(state.requests[user_id], []) {:noreply, new_state} end @impl true def handle_info(:cleanup, state) do now = System.system_time(:millisecond) new_requests = state.requests |> Enum.map(fn {user_id, requests} -> valid = Enum.filter(requests, fn ts -> now - ts < state.window_ms end) {user_id, valid} end) |> Enum.into(%{}) Process.send_after(self(), :cleanup, 60_000) {:noreply, %{state | requests: new_requests}} end end
After (Scala with Akka Typed):
import akka.actor.typed._ import akka.actor.typed.scaladsl._ import scala.concurrent.duration._ object RateLimiter { // Protocol sealed trait Command final case class CheckRate( userId: String, replyTo: ActorRef[RateResponse] ) extends Command final case class Reset(userId: String) extends Command private case object Cleanup extends Command // Responses sealed trait RateResponse final case class Allowed(remaining: Int) extends RateResponse case object RateLimitExceeded extends RateResponse // Configuration case class Config( windowMs: Long = 60000, maxRequests: Int = 100 ) // State private case class State( requests: Map[String, List[Long]], config: Config ) def apply(config: Config = Config()): Behavior[Command] = { Behaviors.setup { context => Behaviors.withTimers { timers => timers.startTimerWithFixedDelay(Cleanup, 60.seconds) active(State(Map.empty, config), context) } } } private def active( state: State, context: ActorContext[Command] ): Behavior[Command] = { Behaviors.receiveMessage { case CheckRate(userId, replyTo) => val now = System.currentTimeMillis() val requests = state.requests.getOrElse(userId, List.empty) // Filter expired requests val validRequests = requests.filter { ts => now - ts < state.config.windowMs } val count = validRequests.length if (count < state.config.maxRequests) { val newRequests = now :: validRequests val newState = state.copy( requests = state.requests + (userId -> newRequests) ) replyTo ! Allowed(state.config.maxRequests - count - 1) active(newState, context) } else { replyTo ! RateLimitExceeded Behaviors.same } case Reset(userId) => val newState = state.copy( requests = state.requests - userId ) active(newState, context) case Cleanup => val now = System.currentTimeMillis() val cleaned = state.requests.map { case (userId, requests) => val valid = requests.filter { ts => now - ts < state.config.windowMs } userId -> valid }.filter(_._2.nonEmpty) active(state.copy(requests = cleaned), context) } } } // Usage val system: ActorSystem[RateLimiter.Command] = ActorSystem(RateLimiter(RateLimiter.Config()), "rate-limiter") implicit val timeout: Timeout = 3.seconds val futureResponse: Future[RateLimiter.RateResponse] = system.ask(ref => RateLimiter.CheckRate("user123", ref))
See Also
For more examples and patterns, see:
- Foundational patterns with cross-language examplesmeta-convert-dev
- Elixir development patternslang-elixir-dev
- Scala development patternslang-scala-dev
- Advanced Akka patternslang-scala-akka-dev
Cross-cutting pattern skills:
- Async, actors, processes across languagespatterns-concurrency-dev
- JSON, validation across languagespatterns-serialization-dev
- Macros, code generation across languagespatterns-metaprogramming-dev