You can use ets (memory cache), dets (disk cache) or mnesia (relational database based on ets and dets).
If you want to use ets or dets, don't forget to start it linked with a gen_fsm, gen_statem or gen_server and make an api to query it. You can do something like that:
-behaviour(gen_server).
-export([init/1]).
-export([handle_call/3]).
init(_) ->
Ets = ets:new(?MODULE, []),
{ok, Ets}.
% forward request to your ets table
% lookup for a key:
handle_call({lookup, Data}, From, Ets) ->
Response = ets:lookup(Data, Ets),
{reply, Response, Ets};
% add new data:
handle_call({insert, Data}, From, Ets) ->
Response = ets:insert(Data, Ets),
{reply, Response, Ets}.
% Rest of you code...
If ETS or DETS doesn't match your need, you can create your own shared data-structure based on available one (dict, orddict, gb_sets, gb_trees, queue, digraph, sets, ordsets...), around standard behaviors. An example based on dict data-structure with gen_server behavior:
-module(gen_dict).
-behaviour(gen_server).
-export([start/0, start/1]).
-export([start_link/0, start_link/1]).
-export([init/1, terminate/2, code_change/3]).
-export([handle_call/3, handle_cast/2, handle_info/2]).
-export([add/2, add/3]).
-export([delete/1, delete/2]).
-export([to_list/0, to_list/1]).
% start without linking new process
start() ->
start([]).
start(Args) ->
gen_server:start({local, ?MODULE}, ?MODULE, Args, []).
% start with link on new process
start_link() ->
start_link([]).
start_link(Args) ->
gen_server:start_link({local, ?MODULE}, ?MODULE, Args, []).
% return by default a dict as state
init(_Args) -> {ok, dict:new()}.
code_change(_,_,_) -> ok.
terminate(_,_) -> ok.
% call request, used if we want to extract data from
% current state
handle_call(size, _From, State) ->
{reply, dict:size(State), State};
handle_call(list, _From, State) ->
{reply, dict:to_list(State), State};
handle_call({find, Pattern}, _From, State) ->
{reply, dict:find(Pattern, State)};
handle_call({map, Fun}, _From, State) ->
{reply, dict:map(Fun, State), State};
handle_call({fold, Fun, Acc}, _From, State) ->
{reply, dict:fold(Fun, Acc, State), State};
handle_call(_Request, _From, State) ->
{reply, bad_call, State}.
% cast request, used if we don't want return
% from our state
handle_cast({add, Key, Value}, State) ->
{noreply, dict:append(Key, Value, State)};
handle_cast({update, Key, Fun}, State) ->
{noreply, dict:update(Key, Fun, State)};
handle_cast({delete, Key}, State) ->
{noreply, dict:erase(Key, State)};
handle_cast({merge, Fun, Dict}, State) ->
{noreply, dict:merge(Fun, Dict, State)};
handle_cast(_Request, State) ->
{noreply, State}.
% info request, currently do nothing
handle_info(_Request, State) ->
{noreply, State}.
% API
% add a new item based on key/value
-spec add(term(), term()) -> ok.
add(Key, Value) ->
add(?MODULE, Key, Value).
-spec add(pid()|atom(), term(), term()) -> ok.
add(Server, Key, Value) ->
gen_server:cast(Server, {add, Key, Value}).
% delete a key
-spec delete(term()) -> ok.
delete(Key) ->
delete(?MODULE, Key).
-spec delete(pid()|atom(), term()) -> ok.
delete(Server, Key) ->
gen_server:cast(Server, {delete, Key}).
% extract state as list
-spec to_list() -> list().
to_list() ->
to_list(?MODULE).
-spec to_list(pid()|atom()) -> list().
to_list(Server) ->
gen_server:call(Server, list).
You can call this code like that:
% compile our code
c(gen_dict).
% start your process
{ok, Process} = gen_dict:start().
% add a new value
gen_dict:add(key, value).
% add another value
gen_dict:add(key2, value2).
% extract as list
List = gen_dict:list().
If you are a bit familiar with Erlang concepts and behaviors, you can make lot of interesting things, like allowing only some process to share a specific structure, or convert one structure to another only with some well crafted processes.