Claude Code Plugins

Community-maintained marketplace

Feedback

lang-erlang-library-dev

@aRustyDev/ai
0
0

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.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name lang-erlang-library-dev
description 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.

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