lang-erlang-library-dev

aRustyDev's avatarfrom aRustyDev

Erlang-specific library development patterns. Use when creating OTP libraries, designing public APIs with process patterns, configuring rebar3, managing application resources, publishing to Hex, or writing EDoc. Extends meta-library-dev with Erlang/OTP tooling and idioms.

⭐1stars🔀1forks📁View on GitHub🕐Updated Jan 4, 2026

When & Why to Use This Skill

This Claude skill provides expert-level patterns and best practices for Erlang/OTP library development. It streamlines the creation of robust, reusable Erlang components by offering specialized guidance on rebar3 configuration, OTP-compliant application structures, and public API design using process-oriented patterns. It also integrates essential ecosystem tools like Hex for package management, Dialyzer for type safety, and EDoc for documentation, ensuring your libraries meet professional Erlang standards and idiomatic practices.

Use Cases

  • Bootstrapping a new Erlang library or OTP application using rebar3 with a standardized project structure and optimized configuration.
  • Designing idiomatic public APIs and custom behaviors to ensure library extensibility, process safety, and clean module organization.
  • Implementing comprehensive testing strategies and quality assurance using EUnit, Common Test, and property-based testing with PropEr.
  • Managing package metadata, versioning, and documentation for publishing high-quality Erlang libraries to the Hex package manager.
  • Configuring Dialyzer and type specifications to enforce static analysis and improve code reliability in complex Erlang systems.
namelang-erlang-library-dev
descriptionErlang-specific library development patterns. Use when creating OTP libraries, designing public APIs with process patterns, configuring rebar3, managing application resources, publishing to Hex, or writing EDoc. Extends meta-library-dev with Erlang/OTP tooling and idioms.

Erlang Library Development

Erlang-specific patterns for OTP library development. This skill extends meta-library-dev with Erlang/OTP tooling, process-oriented design, and ecosystem practices.

This Skill Extends

  • meta-library-dev - Foundational library patterns (API design, versioning, testing strategies)
  • lang-erlang-dev - Core Erlang patterns (processes, OTP behaviors, supervision)

For general concepts like semantic versioning, module organization principles, and testing pyramids, see the meta-skill first. For foundational Erlang patterns, see lang-erlang-dev.

This Skill Adds

  • Erlang tooling: rebar3 configuration, .app.src files, application structure
  • Hex publishing: Package metadata, versioning, documentation
  • Library idioms: Public API design, behavior callbacks, application supervision
  • OTP conventions: Application structure, configuration, resource management
  • EDoc: Documentation generation, type specifications, function docs
  • Dialyzer: Type analysis, PLT files, type specifications

This Skill Does NOT Cover

  • General library patterns - see meta-library-dev
  • Core Erlang/OTP basics - see lang-erlang-dev
  • Web frameworks - see framework-specific skills
  • Distributed systems - see lang-erlang-distributed-dev
  • Performance optimization - see lang-erlang-performance-dev

Quick Reference

Task Command/Pattern
New library app rebar3 new lib <name>
New OTP library rebar3 new app <name>
Compile rebar3 compile
Test rebar3 eunit or rebar3 ct
Type check rebar3 dialyzer
Generate docs rebar3 edoc
Publish (dry run) rebar3 hex publish --dry-run
Publish to Hex rebar3 hex publish
Shell with library rebar3 shell
Run tests rebar3 do eunit, ct

rebar3 Configuration

Basic rebar.config

{erl_opts, [
    debug_info,
    warnings_as_errors,
    warn_export_all,
    warn_unused_import,
    warn_untyped_record
]}.

{deps, []}.

{project_plugins, [rebar3_hex, rebar3_ex_doc]}.

{hex, [
    {doc, #{provider => ex_doc}}
]}.

{profiles, [
    {test, [
        {deps, [
            {proper, "1.4.0"},
            {meck, "0.9.2"}
        ]},
        {erl_opts, [nowarn_export_all]}
    ]},
    {prod, [
        {erl_opts, [no_debug_info, warnings_as_errors]}
    ]}
]}.

{dialyzer, [
    {warnings, [
        unmatched_returns,
        error_handling,
        underspecs
    ]},
    {plt_extra_apps, [ssl, crypto]}
]}.

{xref_checks, [
    undefined_function_calls,
    undefined_functions,
    locals_not_used,
    deprecated_function_calls,
    deprecated_functions
]}.

{cover_enabled, true}.
{cover_opts, [verbose]}.

Application Resource File (.app.src)

{application, mylib,
 [{description, "A library for doing X efficiently"},
  {vsn, "0.1.0"},
  {registered, []},
  {applications, [
      kernel,
      stdlib
  ]},
  {env, []},
  {modules, []},
  {licenses, ["Apache-2.0"]},
  {links, [
      {"GitHub", "https://github.com/username/mylib"},
      {"Hex", "https://hex.pm/packages/mylib"}
  ]},
  {build_tools, ["rebar3"]},
  {files, [
      "src",
      "include",
      "rebar.config",
      "rebar.lock",
      "README.md",
      "LICENSE"
  ]}
 ]}.

OTP Application (.app.src with supervisor)

{application, mylib,
 [{description, "OTP library with supervision tree"},
  {vsn, "0.1.0"},
  {registered, [mylib_sup]},
  {mod, {mylib_app, []}},
  {applications, [
      kernel,
      stdlib,
      sasl
  ]},
  {env, [
      {pool_size, 10},
      {timeout, 5000}
  ]},
  {modules, []},
  {licenses, ["MIT"]},
  {links, [{"GitHub", "https://github.com/username/mylib"}]},
  {build_tools, ["rebar3"]}
 ]}.

Project Structure

Simple Library (No Supervision)

mylib/
├── rebar.config
├── rebar.lock
├── README.md
├── LICENSE
├── src/
│   ├── mylib.app.src       # Application resource file
│   ├── mylib.erl           # Main public API module
│   ├── mylib_parser.erl    # Internal module
│   └── mylib_types.hrl     # Type definitions
├── include/                # Public header files
│   └── mylib.hrl
├── test/
│   ├── mylib_eunit.erl     # EUnit tests
│   └── mylib_SUITE.erl     # Common Test suite
├── doc/                    # Generated by EDoc
│   └── overview.edoc       # Documentation overview
└── priv/                   # Static resources
    └── templates/

OTP Application Library

mylib/
├── rebar.config
├── src/
│   ├── mylib.app.src
│   ├── mylib_app.erl       # Application behavior
│   ├── mylib_sup.erl       # Root supervisor
│   ├── mylib.erl           # Public API
│   ├── mylib_server.erl    # gen_server worker
│   └── mylib_worker.erl    # Worker process
├── include/
│   └── mylib.hrl
├── test/
│   ├── mylib_eunit.erl
│   ├── mylib_SUITE.erl
│   └── mylib_prop.erl      # PropEr property tests
├── doc/
│   └── overview.edoc
└── priv/
    └── config/
        └── defaults.config

Public API Design

Module Organization

Single entry point for users:

-module(mylib).
-export([
    start/0,
    stop/0,
    process/1,
    process/2,
    format/1
]).

%% @doc Start the application
-spec start() -> ok | {error, term()}.
start() ->
    application:ensure_all_started(mylib).

%% @doc Stop the application
-spec stop() -> ok.
stop() ->
    application:stop(mylib).

%% @doc Process input with default options
-spec process(Input :: binary()) -> {ok, term()} | {error, term()}.
process(Input) ->
    process(Input, #{}).

%% @doc Process input with custom options
-spec process(Input :: binary(), Options :: map()) ->
    {ok, term()} | {error, term()}.
process(Input, Options) ->
    mylib_parser:parse(Input, Options).

Behavior Definition

Define custom behaviors for extensibility:

-module(mylib_handler).

%% Behavior callback definitions
-callback init(Args :: term()) -> {ok, State :: term()} | {error, Reason :: term()}.
-callback handle_event(Event :: term(), State :: term()) ->
    {ok, NewState :: term()} | {error, Reason :: term()}.
-callback terminate(Reason :: term(), State :: term()) -> ok.

-optional_callbacks([terminate/2]).

%% Export behavior
-export_type([handler/0]).

-type handler() :: module().

Implement the behavior:

-module(my_custom_handler).
-behaviour(mylib_handler).

-export([init/1, handle_event/2, terminate/2]).

init(Args) ->
    {ok, #{args => Args}}.

handle_event(Event, State) ->
    io:format("Received: ~p~n", [Event]),
    {ok, State}.

terminate(_Reason, _State) ->
    ok.

Options and Configuration

Use maps for flexible options:

-module(mylib_config).
-export([parse_options/1, defaults/0]).

-type options() :: #{
    timeout => pos_integer(),
    retry => boolean(),
    max_retries => pos_integer(),
    format => json | xml | binary
}.

-export_type([options/0]).

%% @doc Default configuration
-spec defaults() -> options().
defaults() ->
    #{
        timeout => 5000,
        retry => true,
        max_retries => 3,
        format => json
    }.

%% @doc Merge user options with defaults
-spec parse_options(UserOpts :: map()) -> options().
parse_options(UserOpts) ->
    maps:merge(defaults(), UserOpts).

Type Specifications and Dialyzer

Complete Type Specs

-module(mylib_types).

%% Exported types
-export_type([
    user_id/0,
    user/0,
    result/0,
    error_reason/0
]).

%% Type definitions
-type user_id() :: non_neg_integer().

-type user() :: #{
    id := user_id(),
    name := binary(),
    email := binary(),
    created_at := calendar:datetime()
}.

-type error_reason() ::
    not_found |
    invalid_input |
    timeout |
    {internal_error, term()}.

-type result() :: {ok, term()} | {error, error_reason()}.

%% Records with types
-record(config, {
    timeout :: pos_integer(),
    max_connections :: pos_integer(),
    handler :: module()
}).

-type config() :: #config{}.
-export_type([config/0]).

Function Specifications

-module(mylib_api).

%% @doc Create a new user
-spec create_user(Name :: binary(), Email :: binary()) ->
    {ok, mylib_types:user()} | {error, mylib_types:error_reason()}.
create_user(Name, Email) when is_binary(Name), is_binary(Email) ->
    UserId = generate_id(),
    User = #{
        id => UserId,
        name => Name,
        email => Email,
        created_at => calendar:universal_time()
    },
    {ok, User}.

%% @doc Find user by ID
-spec find_user(mylib_types:user_id()) ->
    {ok, mylib_types:user()} | {error, not_found}.
find_user(UserId) when is_integer(UserId), UserId >= 0 ->
    case lookup_user(UserId) of
        undefined -> {error, not_found};
        User -> {ok, User}
    end.

%% Internal function - no spec needed (dialyzer infers)
lookup_user(UserId) ->
    ets:lookup(users_table, UserId).

Dialyzer PLT Management

% rebar.config
{dialyzer, [
    {warnings, [
        unmatched_returns,
        error_handling,
        underspecs,
        unknown
    ]},
    {plt_apps, all_deps},
    {plt_extra_apps, [ssl, crypto, public_key]},
    {plt_location, local},
    {base_plt_location, global}
]}.

EDoc Documentation

Module Documentation

%%% @doc Main API module for MyLib.
%%%
%%% This module provides the primary interface for working with MyLib.
%%% All operations are safe to use from multiple processes concurrently.
%%%
%%% == Quick Start ==
%%%
%%% ```
%%% 1> mylib:start().
%%% ok
%%% 2> {ok, Result} = mylib:process(<<"input">>).
%%% {ok, #{data => <<"processed">>}}
%%% '''
%%%
%%% == Configuration ==
%%%
%%% The application can be configured via application environment:
%%%
%%% ```
%%% {mylib, [
%%%     {timeout, 10000},
%%%     {pool_size, 20}
%%% ]}
%%% '''
%%%
%%% @end
-module(mylib).

-author("Your Name <your.email@example.com>").
-copyright("2025 Your Name").
-version("0.1.0").

Function Documentation

%%% @doc Process input data with options.
%%%
%%% This function processes the input binary according to the provided
%%% options and returns the result. Processing is done asynchronously
%%% in a worker pool.
%%%
%%% == Options ==
%%%
%%% <ul>
%%%   <li>`timeout' - Maximum time in milliseconds (default: 5000)</li>
%%%   <li>`format' - Output format: `json' or `binary' (default: json)</li>
%%%   <li>`retry' - Whether to retry on failure (default: true)</li>
%%% </ul>
%%%
%%% == Examples ==
%%%
%%% ```
%%% %% Simple processing
%%% {ok, Result} = mylib:process(<<"data">>).
%%%
%%% %% With custom timeout
%%% {ok, Result} = mylib:process(<<"data">>, #{timeout => 10000}).
%%%
%%% %% Binary output format
%%% {ok, Binary} = mylib:process(<<"data">>, #{format => binary}).
%%% '''
%%%
%%% @see process/1
%%% @end
-spec process(Input :: binary(), Options :: map()) ->
    {ok, term()} | {error, term()}.
process(Input, Options) ->
    % Implementation
    ok.

Type Documentation

%%% @type user_id() = non_neg_integer().
%%% Unique identifier for a user.

%%% @type user() = #{
%%%     id := user_id(),
%%%     name := binary(),
%%%     email := binary(),
%%%     created_at := calendar:datetime()
%%% }.
%%% User record containing all user information.

%%% @type error_reason() =
%%%     not_found |
%%%     invalid_input |
%%%     timeout |
%%%     {internal_error, term()}.
%%% Possible error reasons returned by library functions.

Overview Documentation

Create doc/overview.edoc:

@author Your Name <your.email@example.com>
@copyright 2025 Your Name
@version 0.1.0
@title MyLib - Efficient Data Processing Library
@doc

== Overview ==

MyLib is a high-performance library for processing data in Erlang/OTP
applications. It provides a simple, consistent API while leveraging
OTP principles for reliability and scalability.

== Features ==

<ul>
  <li>Concurrent processing with worker pools</li>
  <li>Automatic retries and error handling</li>
  <li>Multiple output formats</li>
  <li>Full type specifications</li>
  <li>Comprehensive test coverage</li>
</ul>

== Installation ==

Add to your `rebar.config':

{deps, [ {mylib, "0.1.0"} ]}. '''

== Quick Start ==

%% Start the application
ok = mylib:start().

%% Process some data
{ok, Result} = mylib:process(<<"input data">>).

%% Stop the application
ok = mylib:stop().
'''

@end

Testing Patterns

EUnit Tests

-module(mylib_eunit).
-include_lib("eunit/include/eunit.hrl").

%%% Setup/Teardown

setup() ->
    application:ensure_all_started(mylib).

cleanup(_) ->
    application:stop(mylib).

%%% Test Fixtures

mylib_test_() ->
    {setup,
     fun setup/0,
     fun cleanup/1,
     [
         {"Process valid input", fun test_process_valid/0},
         {"Process invalid input", fun test_process_invalid/0},
         {"Process with timeout", fun test_process_timeout/0}
     ]}.

%%% Tests

test_process_valid() ->
    Input = <<"valid input">>,
    {ok, Result} = mylib:process(Input),
    ?assertMatch(#{data := _}, Result).

test_process_invalid() ->
    Input = <<"">>,
    ?assertEqual({error, invalid_input}, mylib:process(Input)).

test_process_timeout() ->
    Input = <<"data">>,
    Options = #{timeout => 1},
    ?assertMatch({error, timeout}, mylib:process(Input, Options)).

%%% Property-Based Tests

prop_never_crashes_test_() ->
    {timeout, 60, fun() ->
        ?assert(proper:quickcheck(prop_no_crash(), [{numtests, 1000}]))
    end}.

prop_no_crash() ->
    ?FORALL(Input, binary(),
        case mylib:process(Input) of
            {ok, _} -> true;
            {error, _} -> true
        end
    ).

Common Test Suites

-module(mylib_SUITE).
-include_lib("common_test/include/ct.hrl").

%% CT callbacks
-export([all/0, groups/0, init_per_suite/1, end_per_suite/1]).
-export([init_per_testcase/2, end_per_testcase/2]).

%% Test cases
-export([
    test_basic_processing/1,
    test_concurrent_processing/1,
    test_error_handling/1
]).

%%% CT Callbacks

all() ->
    [
        {group, basic},
        {group, concurrent},
        {group, errors}
    ].

groups() ->
    [
        {basic, [parallel], [test_basic_processing]},
        {concurrent, [parallel], [test_concurrent_processing]},
        {errors, [sequence], [test_error_handling]}
    ].

init_per_suite(Config) ->
    {ok, _} = application:ensure_all_started(mylib),
    Config.

end_per_suite(_Config) ->
    application:stop(mylib),
    ok.

init_per_testcase(_TestCase, Config) ->
    Config.

end_per_testcase(_TestCase, _Config) ->
    ok.

%%% Test Cases

test_basic_processing(Config) ->
    Input = <<"test data">>,
    {ok, Result} = mylib:process(Input),
    ct:log("Result: ~p", [Result]),
    #{data := Data} = Result,
    true = is_binary(Data).

test_concurrent_processing(Config) ->
    Inputs = [<<"input", (integer_to_binary(N))/binary>> || N <- lists:seq(1, 100)],

    Self = self(),
    [spawn(fun() ->
        {ok, _} = mylib:process(Input),
        Self ! {done, N}
    end) || {N, Input} <- lists:enumerate(Inputs)],

    receive_n(100).

test_error_handling(Config) ->
    {error, invalid_input} = mylib:process(<<>>),
    {error, timeout} = mylib:process(<<"data">>, #{timeout => 1}).

%%% Helpers

receive_n(0) -> ok;
receive_n(N) ->
    receive
        {done, _} -> receive_n(N - 1)
    after 5000 ->
        ct:fail("Timeout waiting for concurrent operations")
    end.

PropEr Property Tests

-module(mylib_prop).
-include_lib("proper/include/proper.hrl").
-include_lib("eunit/include/eunit.hrl").

%%% Generators

user_id() ->
    non_neg_integer().

user_name() ->
    ?LET(Len, range(1, 50),
        ?LET(Chars, vector(Len, range($a, $z)),
            list_to_binary(Chars))).

valid_user() ->
    ?LET({Id, Name, Email}, {user_id(), user_name(), user_name()},
        #{id => Id, name => Name, email => <<Email/binary, "@example.com">>}).

%%% Properties

prop_create_user_returns_user() ->
    ?FORALL({Name, Email}, {user_name(), user_name()},
        begin
            {ok, User} = mylib:create_user(Name, Email),
            maps:is_key(id, User) andalso
            maps:get(name, User) =:= Name andalso
            maps:get(email, User) =:= Email
        end).

prop_roundtrip_serialization() ->
    ?FORALL(User, valid_user(),
        begin
            Serialized = mylib:serialize(User),
            {ok, Deserialized} = mylib:deserialize(Serialized),
            User =:= Deserialized
        end).

%%% EUnit wrapper

properties_test_() ->
    {timeout, 120, [
        ?_assert(proper:quickcheck(prop_create_user_returns_user(), [{numtests, 100}])),
        ?_assert(proper:quickcheck(prop_roundtrip_serialization(), [{numtests, 100}]))
    ]}.

Publishing to Hex

Hex Package Configuration

Add to rebar.config:

{project_plugins, [
    rebar3_hex,
    rebar3_ex_doc
]}.

{hex, [
    {doc, #{provider => ex_doc}}
]}.

{ex_doc, [
    {source_url, <<"https://github.com/username/mylib">>},
    {extras, [<<"README.md">>, <<"LICENSE">>, <<"CHANGELOG.md">>]},
    {main, <<"readme">>}
]}.

Pre-publish Checklist

  • Version bumped in .app.src
  • CHANGELOG.md updated with version changes
  • README.md is current and comprehensive
  • All tests pass: rebar3 do eunit, ct
  • Dialyzer passes: rebar3 dialyzer
  • Documentation builds: rebar3 edoc
  • Application metadata complete in .app.src
  • License file(s) included
  • No uncommitted changes
  • Dry run succeeds: rebar3 hex publish --dry-run

Publishing Commands

# Generate documentation
rebar3 hex docs

# Dry run to check package
rebar3 hex publish --dry-run

# Actually publish
rebar3 hex publish

# Publish documentation separately
rebar3 hex publish docs

# Revert a package (within 24h or 1 hour for new packages)
rebar3 hex revert 0.1.0

# Retire a version (soft deprecation)
rebar3 hex retire mylib 0.1.0 other "Please upgrade to 0.2.0"

Version Retirement Reasons

# Available retirement reasons:
rebar3 hex retire mylib VERSION other "Custom message"
rebar3 hex retire mylib VERSION security "Security vulnerability found"
rebar3 hex retire mylib VERSION deprecated "Use new-package instead"
rebar3 hex retire mylib VERSION invalid "Invalid release"
rebar3 hex retire mylib VERSION renamed "Package renamed to new-name"

Application Supervision

Application Behavior

-module(mylib_app).
-behaviour(application).

-export([start/2, stop/1]).

start(_StartType, _StartArgs) ->
    case mylib_sup:start_link() of
        {ok, Pid} ->
            {ok, Pid};
        Error ->
            Error
    end.

stop(_State) ->
    ok.

Root Supervisor

-module(mylib_sup).
-behaviour(supervisor).

-export([start_link/0, init/1]).

start_link() ->
    supervisor:start_link({local, ?MODULE}, ?MODULE, []).

init([]) ->
    SupFlags = #{
        strategy => one_for_one,
        intensity => 5,
        period => 60
    },

    ChildSpecs = [
        #{
            id => mylib_server,
            start => {mylib_server, start_link, []},
            restart => permanent,
            shutdown => 5000,
            type => worker,
            modules => [mylib_server]
        },
        #{
            id => mylib_pool_sup,
            start => {mylib_pool_sup, start_link, []},
            restart => permanent,
            shutdown => infinity,
            type => supervisor,
            modules => [mylib_pool_sup]
        }
    ],

    {ok, {SupFlags, ChildSpecs}}.

Worker Pool Supervisor

-module(mylib_pool_sup).
-behaviour(supervisor).

-export([start_link/0, init/1]).

start_link() ->
    supervisor:start_link({local, ?MODULE}, ?MODULE, []).

init([]) ->
    PoolSize = application:get_env(mylib, pool_size, 10),

    SupFlags = #{
        strategy => one_for_one,
        intensity => 10,
        period => 60
    },

    ChildSpecs = [
        #{
            id => {mylib_worker, N},
            start => {mylib_worker, start_link, [N]},
            restart => permanent,
            shutdown => 5000,
            type => worker,
            modules => [mylib_worker]
        }
        || N <- lists:seq(1, PoolSize)
    ],

    {ok, {SupFlags, ChildSpecs}}.

Configuration Management

Application Environment

% src/mylib.app.src
{application, mylib,
 [{env, [
      {pool_size, 10},
      {timeout, 5000},
      {retry_count, 3},
      {log_level, info}
  ]}
 ]}.

Runtime Configuration

-module(mylib_config).
-export([get/1, get/2, set/2]).

%% @doc Get configuration value
-spec get(Key :: atom()) -> term().
get(Key) ->
    case application:get_env(mylib, Key) of
        {ok, Value} -> Value;
        undefined -> error({config_not_found, Key})
    end.

%% @doc Get configuration value with default
-spec get(Key :: atom(), Default :: term()) -> term().
get(Key, Default) ->
    application:get_env(mylib, Key, Default).

%% @doc Set configuration value at runtime
-spec set(Key :: atom(), Value :: term()) -> ok.
set(Key, Value) ->
    application:set_env(mylib, Key, Value).

Config Files (sys.config)

% config/sys.config
[
    {mylib, [
        {pool_size, 20},
        {timeout, 10000},
        {log_level, debug}
    ]},
    {sasl, [
        {sasl_error_logger, {file, "log/sasl-error.log"}},
        {errlog_type, error}
    ]},
    {kernel, [
        {logger_level, info}
    ]}
].

Common Patterns

Singleton Server

-module(mylib_server).
-behaviour(gen_server).

%% API
-export([start_link/0, call/1, cast/1, get_state/0]).

%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2]).

-define(SERVER, ?MODULE).

%%% API

start_link() ->
    gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).

call(Request) ->
    gen_server:call(?SERVER, Request).

cast(Request) ->
    gen_server:cast(?SERVER, Request).

get_state() ->
    gen_server:call(?SERVER, get_state).

%%% Callbacks

init([]) ->
    State = #{
        started_at => erlang:system_time(second),
        requests => 0
    },
    {ok, State}.

handle_call(get_state, _From, State) ->
    {reply, State, State};
handle_call(Request, _From, State = #{requests := Count}) ->
    Result = process_request(Request),
    {reply, Result, State#{requests => Count + 1}}.

handle_cast(_Msg, State) ->
    {noreply, State}.

handle_info(_Info, State) ->
    {noreply, State}.

terminate(_Reason, _State) ->
    ok.

%%% Internal

process_request(Request) ->
    {ok, Request}.

Resource Pool Pattern

-module(mylib_pool).
-export([checkout/0, checkin/1, with_resource/1]).

-define(POOL, mylib_resource_pool).

%% @doc Check out a resource from the pool
-spec checkout() -> {ok, pid()} | {error, no_resources}.
checkout() ->
    case ets:lookup(?POOL, available) of
        [{available, [Pid | Rest]}] ->
            ets:insert(?POOL, {available, Rest}),
            ets:insert(?POOL, {in_use, Pid}),
            {ok, Pid};
        _ ->
            {error, no_resources}
    end.

%% @doc Return a resource to the pool
-spec checkin(pid()) -> ok.
checkin(Pid) ->
    ets:delete_object(?POOL, {in_use, Pid}),
    [{available, Available}] = ets:lookup(?POOL, available),
    ets:insert(?POOL, {available, [Pid | Available]}),
    ok.

%% @doc Execute function with a resource
-spec with_resource(fun((pid()) -> term())) -> {ok, term()} | {error, term()}.
with_resource(Fun) ->
    case checkout() of
        {ok, Resource} ->
            try
                Result = Fun(Resource),
                {ok, Result}
            after
                checkin(Resource)
            end;
        Error ->
            Error
    end.

Anti-Patterns

1. Exposing Internal Processes

% Bad: Exposes internal PIDs
-spec get_worker() -> pid().
get_worker() ->
    whereis(mylib_worker).

% Good: Provide functional API
-spec process(Input :: term()) -> {ok, term()} | {error, term()}.
process(Input) ->
    mylib_worker:process(Input).

2. Breaking Module Contracts

% Bad: Changing return type in new version
% v0.1.0
-spec parse(binary()) -> map().

% v0.2.0 - BREAKING
-spec parse(binary()) -> {ok, map()} | {error, term()}.

% Good: Add new function, deprecate old
% v0.2.0
-spec parse(binary()) -> map().  % Deprecated
-spec parse_safe(binary()) -> {ok, map()} | {error, term()}.

3. Ignoring Application Lifecycle

% Bad: Starting processes in module
-module(mylib).
start_worker() ->
    spawn(fun() -> worker_loop() end).

% Good: Use supervision tree
-module(mylib_sup).
init([]) ->
    ChildSpecs = [worker_spec()],
    {ok, {sup_flags(), ChildSpecs}}.

4. Missing Type Specs

% Bad: No specs, dialyzer can't verify
process(Input) ->
    transform(Input).

% Good: Full specifications
-spec process(Input :: binary()) -> {ok, term()} | {error, atom()}.
process(Input) when is_binary(Input) ->
    transform(Input).

References