Repository: ihnokim/codepack (language: python)

Issue:
Feature engine and worker refactoring



Discussion / hints:

Fix patch:
diff --git a/.gitignore b/.gitignore
index bf820e9..b5a98f8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,8 @@
 *.idea*
 *.ipynb
 htmlcov/*
+dist/*
+build/*
+codepack.egg-info/*
+logs/*
+*.zip
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..772fff9
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,38 @@
+FROM python:3.7-slim
+MAINTAINER Inho Kim, ihnokim58@gmail.com
+
+# ARG http_proxy=http://x.x.x.x:x
+# ARG https_proxy=http://x.x.x.x:x
+# COPY ca.crt /usr/local/share/ca-certificates/
+# RUN update-ca-certificates
+
+RUN apt-get update && apt-get -y upgrade && apt-get -y dist-upgrade && apt-get install -y alien libaio1 libsnappy-dev wget vim
+RUN wget https://yum.oracle.com/repo/OracleLinux/OL7/oracle/instantclient/x86_64/getPackage/oracle-instantclient19.3-basiclite-19.3.0.0.0-1.x86_64.rpm --no-check-certificate
+RUN alien -i --scripts oracle-instantclient*.rpm
+RUN rm -f oracle-instantclient19.3*.rpm && apt-get -y autoremove && apt-get -y clean
+
+RUN mkdir -p /opt/codepack
+WORKDIR /opt/codepack
+
+RUN mkdir -p config
+RUN mkdir -p logs
+RUN mkdir -p scripts
+RUN mkdir -p jupyter/notebook
+RUN mkdir -p app
+
+COPY entry_point.sh .
+COPY extra-requirements.txt .
+COPY config config
+COPY scripts scripts
+COPY app app
+
+RUN chmod 755 entry_point.sh
+
+ENV CODEPACK_CONFIG_DIR /opt/codepack/config
+ENV CODEPACK_CONFIG_PATH default.ini
+ENV CODEPACK_LOGGER_LOG_DIR /opt/codepack/logs
+
+RUN python -m pip install --upgrade pip --trusted-host pypi.org --trusted-host pypi.python.org --trusted-host files.pythonhosted.org
+RUN pip install --upgrade --trusted-host pypi.python.org --trusted-host pypi.org --trusted-host files.pythonhosted.org -r extra-requirements.txt
+
+ENTRYPOINT ["./entry_point.sh"]
diff --git a/README.rst b/README.rst
index 5efccdb..60166a8 100644
--- a/README.rst
+++ b/README.rst
@@ -1,9 +1,283 @@
+========
+CodePack
+========
+
 .. image:: https://github.com/ihnokim/codepack/workflows/Test/badge.svg?event=push&branch=master
   :target: https://github.com/ihnokim/codepack/actions?query=workflow%3ATest+event%3Apush+branch%3Amaster
   :alt: Test Status
 .. image:: https://coveralls.io/repos/github/ihnokim/codepack/badge.svg
   :target: https://coveralls.io/github/ihnokim/codepack
   :alt: Code Coverage
+.. image:: https://img.shields.io/pypi/v/codepack
+  :target: https://pypi.org/project/codepack/
+  :alt: PyPI - Package Version
+.. image:: https://img.shields.io/pypi/pyversions/codepack
+  :target: https://pypi.org/project/codepack/
+  :alt: PyPI - Python Version
 
-CodePack
------
+CodePack is a Python package to easily make, run, and manage workflows.
+You can write a task which is called Code with pure python code.
+By simply making linkages between the Codes, you can easily build a workflow which is called CodePack.
+The CodePack can be either executed or scheduled
+(via `APScheduler <https://apscheduler.readthedocs.io/en/master/?badge=latest>`_)
+with a set of arguments which is called ArgPack.
+The Code, CodePack, and ArgPack support JSON serialization and deserialization,
+so they can be saved to a file or external storage and recycled to create different combinations.
+
+CodePack is one of the good choices to represent flexible workflows.
+Business environments often require frequent changes to workflows to quickly meet volatile customer requirements.
+CodePack provides various plugins to configure the whole system.
+For example, file systems, MongoDB, and AWS S3 can be options
+for transferring data between Codes or storing serialized objects.
+You can easily setup the various components by modifying few lines in the configuration file
+or adding some OS environment variables.
+
+Simple is the best ;) but sometimes...
+you may need helps from rich user interfaces and powerful utilities
+to troubleshoot complex workflows and expand your system.
+It is recommended to convert CodePacks
+into the DAGs used in `Apache Airflow <https://airflow.apache.org/docs/apache-airflow/stable/>`_
+especially when you deal with workflows that are mostly static and slowly changing.
+CodePack is also planning to provide rich APIs and user interfaces, so please stay tuned!
+
+The key features are:
+
+- **Easy**: Designed to be easy to use with pure python code. There is nothing new to learn!
+- **Simple**: Minimize the effort of configuring the entire system to your taste.
+- **Responsive**: Get workflow results immediately or later by sync/async execution or scheduling
+  right after making changes in the workflow and its arguments.
+- **Transformable**: Convert almost everything into JSON, so you can easily extend its use to other solutions.
+
+Requirements
+------------
+
+CodePack is tested with Python 3.6, 3.7, 3.8, and 3.9.
+
+Installation
+------------
+
+Use :code:`pip install` command to install CodePack into your Python environment.
+Check `PyPI <https://pypi.org/project/codepack/>`_ for more details.
+
+.. code-block::
+
+  $ pip install codepack
+
+If you want to try sample apps provided in `app <https://github.com/ihnokim/codepack/tree/master/app>`_,
+you need to install additional packages.
+
+.. code-block::
+
+  $ pip install codepack jupyter uvicorn[standard] fastapi
+
+To run unit tests, use following commands.
+
+.. code-block::
+
+  $ pip install pytest pytest-cov mongomock
+  $ pytest --cov codepack tests
+
+Example
+-------
+
+**1. Instantiate and call Code**
+
+Just wrap a normal python function with Code and use it the way you are used to it.
+A Code has its own id.
+If you do not specify an id when creating a Code,
+the given function name is automatically set to id.
+
+.. code-block:: python
+
+  from codepack import Code
+
+  def add3(a, b=1, c=2):
+    return a + b + c
+
+  code = Code(add3)
+  result = code(3, b=5)  # result == 10
+
+**2. Link Codes**
+
+You can create links between Codes with :code:`>>` operator and break links with :code:`//` operator.
+The linkage represents the order of execution between Codes.
+If you want to transfer the result of a preceding Code to the following Code,
+use :code:`receive` method and :code:`<<` operator to represent the data dependency.
+
+.. code-block:: python
+
+  code1 >> code2 >> code3
+  code3 >> [code4, code5]
+  code3 // code5
+
+  code2.receive('a') << code1
+  code4.receive('c') << code3
+
+**3. Make a CodePack**
+
+Tag any one of the connected Codes to a CodePack in :code:`code`,
+and specify the Code to return the final result in :code:`subscribe`.
+If you don't specify the subscription, the CodePack returns :code:`None`.
+
+.. code-block:: python
+
+  from codepack import CodePack
+
+  codepack = CodePack('my_codepack', code=code1, subscribe=code4)
+
+**4. Set arguments and run CodePack**
+
+Don't bother yourself to remember all argument specifications in Codes.
+You can extract an empty ArgPack from the CodePack.
+ArgPack even understands the default keyword arguments in Codes.
+All you need to do is just fill out the blanks!
+
+.. code-block:: python
+
+  from codepack import ArgPack
+
+  argpack = codepack.make_argpack()
+
+  argpack['add2'](x=3, y=5)
+  argpack['add3'](a=3)  # receive c from add2
+  argpack['mul2'](x2=2)  # receive x1 from add3
+
+  result = codepack(argpack)  # subscribe mul2
+
+**5. Save and load**
+
+You can save Code, CodePack, and ArgPack into somewhere you specified in configuration.
+Each element has its own id, so it can be retrieved and reused by querying the id.
+
+.. code-block:: python
+
+  from codepack import Default
+
+  code_storage = Default.get_service('code', 'storage_service')
+  codepack_storage = Default.get_service('codepack', 'storage_service')
+
+  code.save()
+  code = code_storage.load('add2')
+
+  codepack.save()
+  codepack = codepack_storage.load('my_codepack')
+
+  codepack.save_argpack(argpack)  # codepack.id == argpack.id
+  argpack = codepack.load_argpack('my_codepack')
+
+**6. JSON serialization and deserialization**
+
+Code, CodePack, and ArgPack are interchangeable with JSON.
+Use these guys everywhere!
+
+.. code-block:: python
+
+  code_json = code.to_json()
+  code_py = Code.from_json(code_json)
+
+  codepack_json = codepack.to_json()
+  codepack_py = CodePack.from_json(codepack_json)
+
+  argpack_json = argpack.to_json()
+  argpack_py = ArgPack.from_json(argpack_json)
+
+**7. Set configuration**
+
+There are many plugins to manage Codes, CodePacks, and ArgPacks.
+You can easily configure the plugins by using a
+`configuration file <https://github.com/ihnokim/codepack/blob/master/config/codepack.ini>`_
+or adding some OS environment variables.
+If you don't specify any configuration files,
+CodePack uses the
+`default configuration file <https://github.com/ihnokim/codepack/blob/master/codepack/config/default/default.ini>`_
+located at :code:`codepack/config/default/default.ini`.
+The default configuration file can be replaced with your custom configuration file
+by adding an OS environment variable: :code:`CODEPACK_CONFIG_PATH=/path/to/your-configuration-file.ini`.
+
+Let's say you want the following scenario:
+
+- Save and load Codes through MongoDB.
+- Exchange data between Codes through files.
+
+Your configuration file should include:
+
+.. code-block::
+
+  [code]
+  source = mongodb
+  db = codepack
+  collection = codes
+
+  [delivery]
+  source = file
+  path = /data/delivery
+
+  [mongodb]
+  host = server1.ip,server2.ip,server3.ip
+  port = 27017
+  username = admin
+  password = ***
+  replicaSet = test
+
+When the external service like MongoDB or AWS S3 is set to :code:`source`,
+the connection information should also be included.
+
+If it is too annoying to modify configuration file,
+you can overwrite configuration values with OS environment variables:
+:code:`CODEPACK_<SECTION>_<KEY>=<VALUE>`.
+For example, if the IP address of the MongoDB has changed to localhost,
+set :code:`CODEPACK_MONGODB_HOST=localhost`.
+
+The configuration is accessible from code via :code:`Config` and :code:`Default` classes.
+
+.. code-block:: python
+
+  from codepack import Config, Default
+  from codepack.interface import MongoDB
+
+  config = Config()
+  mongodb_config = config.get_config('mongodb')
+  mongodb = MongoDB(mongodb_config)
+  document = mongodb.test_db.test_collection.find_one({'_id': 'test'})
+
+  code_storage_service = Default.get_service('code', 'storage_service')
+  delivery_service = Default.get_service('delivery', 'delivery_service')
+  scheduler = Default.get_scheduler()
+  logger = Default.get_logger()
+
+**8. To use in other machines...**
+
+A Code should be executable in isolated environments different from where it was created.
+In order to avoid the python package dependency issue,
+put import statements into the function.
+
+.. code-block:: python
+
+  def calc_numpy_array_sum(x):
+    import numpy as np
+    return np.sum(np.array(x))
+
+This little trick is very important when setting up distributed systems
+where each Code in a CodePack runs on a different machine.
+
+**9. The other features**
+
+For more information on schedulers, asynchronous workers, APIs, and other features,
+please see Documentation and `Github <https://github.com/ihnokim/codepack>`_.
+
+Source code
+-----------
+
+The source can be browsed at `Github <https://github.com/ihnokim/codepack>`_.
+
+Contributing
+------------
+
+Want to help CodePack?
+Feel free to use `Issues <https://github.com/ihnokim/codepack/issues>`_
+and `Discussions <https://github.com/ihnokim/codepack/discussions>`_ to unleash your imagination!
+
+License
+-------
+
+This project is licensed under the terms of the MIT license.
diff --git a/app/apiserver/dependencies/__init__.py b/app/apiserver/dependencies/__init__.py
index 04acb61..e0c935e 100644
--- a/app/apiserver/dependencies/__init__.py
+++ b/app/apiserver/dependencies/__init__.py
@@ -1,4 +1,4 @@
-from .common import Common
+from codepack.utils import Common
 
 
 common = Common()
diff --git a/app/apiserver/dependencies/common.py b/app/apiserver/dependencies/common.py
deleted file mode 100644
index 9f7536c..0000000
--- a/app/apiserver/dependencies/common.py
+++ /dev/null
@@ -1,21 +0,0 @@
-class Common:
-    def __init__(self):
-        self.variables = dict()
-
-    def __getitem__(self, item):
-        return self.variables[item]
-
-    def __setitem__(self, key, value):
-        self.variables[key] = value
-
-    def __getattr__(self, item):
-        return self.__getitem__(item)
-
-    def keys(self):
-        return self.variables.keys()
-
-    def values(self):
-        return self.variables.values()
-
-    def items(self):
-        return self.variables.items()
diff --git a/app/apiserver/main.py b/app/apiserver/main.py
index d59d14f..54dd8a5 100644
--- a/app/apiserver/main.py
+++ b/app/apiserver/main.py
@@ -1,7 +1,6 @@
-from codepack.scheduler import Scheduler
-from codepack.config import Default
+from codepack import Scheduler, Default
+from codepack.plugin.service import Service
 from fastapi import FastAPI, Request
-from codepack.employee import Supervisor
 from .routers import code, codepack, argpack, scheduler
 from .dependencies import common
 
@@ -26,37 +25,25 @@ async def add_process_time_header(request: Request, call_next):
 async def startup():
     Default.init()
     config = Default.config.get_config(section='apiserver')
-    supervisor = config.get('supervisor', 'self')
     _scheduler = config.get('scheduler', 'self')
-    if isinstance(supervisor, Supervisor):
-        common['supervisor'] = supervisor
-    elif isinstance(supervisor, str):
-        if supervisor == 'self':
-            common['supervisor'] = Default.get_employee('supervisor')
-        else:
-            raise NotImplementedError("supervisor should be 'self', not '%s'")
+    common.add(key='supervisor', value=Default.get_employee('supervisor'), destroy=destroy_supervisor)
     if isinstance(_scheduler, Scheduler):
-        common['scheduler'] = _scheduler
+        common.add(key='scheduler', value=_scheduler, destroy=destroy_scheduler)
         common.scheduler.start()
     elif isinstance(_scheduler, str):
         if _scheduler == 'self':
-            common['scheduler'] = Default.get_scheduler()
+            common.add(key='scheduler', value=Default.get_scheduler(), destroy=destroy_scheduler)
             common.scheduler.start()
         else:
-            common['scheduler'] = _scheduler
+            common.add(key='scheduler', value=_scheduler)
 
 
 @app.on_event('shutdown')
 async def shutdown():
-    common.supervisor.producer.close()
-    for service in Default.instances.values():
-        if hasattr(service, 'mongodb'):
-            service.mongodb.close()
-    if isinstance(common.scheduler, Scheduler):
-        if common.scheduler.is_running():
-            common.scheduler.stop()
-        if hasattr(common.scheduler, 'mongodb'):
-            common.scheduler.mongodb.close()
+    for instance in Default.instances.values():
+        if isinstance(instance, Service):
+            instance.storage.close()
+    common.clear()
 
 
 @app.get('/organize')
@@ -69,3 +56,13 @@ async def organize():
 async def organize(serial_number: str):
     common.supervisor.organize(serial_number=serial_number)
     return None
+
+
+def destroy_supervisor(x):
+    x.close()
+
+
+def destroy_scheduler(x):
+    if isinstance(x, Scheduler):
+        if x.is_running():
+            x.stop()
diff --git a/app/apiserver/models/scheduler.py b/app/apiserver/models/scheduler.py
index ac1e7b7..0d249a1 100644
--- a/app/apiserver/models/scheduler.py
+++ b/app/apiserver/models/scheduler.py
@@ -4,7 +4,7 @@
 class Job(BaseModel):
     job_id: str = None
     trigger: str
-    trigger_config = dict
+    trigger_config: dict
 
 
 class CodePackIDJob(Job):
diff --git a/app/apiserver/routers/argpack.py b/app/apiserver/routers/argpack.py
index a58354f..b545d92 100644
--- a/app/apiserver/routers/argpack.py
+++ b/app/apiserver/routers/argpack.py
@@ -1,7 +1,6 @@
+from codepack import Default, ArgPack
 from fastapi import APIRouter
 from ..models.argpack import ArgPackJSON
-from codepack.config import Default
-from codepack.argpack import ArgPack
 
 
 router = APIRouter(
@@ -13,7 +12,7 @@
 
 @router.post('/save')
 async def save(argpack: ArgPackJSON):
-    storage_service = Default.get_storage_instance('argpack', 'storage_service')
+    storage_service = Default.get_service('argpack', 'storage_service')
     tmp = ArgPack.from_json(argpack.argpack)
     storage_service.save(item=tmp)
     return {'id': tmp.id}
@@ -21,7 +20,7 @@ async def save(argpack: ArgPackJSON):
 
 @router.post('/update')
 async def update(argpack: ArgPackJSON):
-    storage_service = Default.get_storage_instance('argpack', 'storage_service')
+    storage_service = Default.get_service('argpack', 'storage_service')
     tmp = ArgPack.from_json(argpack.argpack)
     storage_service.save(item=tmp, update=True)
     return {'id': tmp.id}
@@ -29,13 +28,13 @@ async def update(argpack: ArgPackJSON):
 
 @router.get('/remove/{id}')
 async def remove(id: str):
-    storage_service = Default.get_storage_instance('argpack', 'storage_service')
+    storage_service = Default.get_service('argpack', 'storage_service')
     storage_service.remove(id)
     return {'id': id}
 
 
 @router.get('/load/{id}')
 async def load(id: str):
-    storage_service = Default.get_storage_instance('argpack', 'storage_service')
+    storage_service = Default.get_service('argpack', 'storage_service')
     argpack = storage_service.load(id)
     return {'argpack': argpack.to_json()}
diff --git a/app/apiserver/routers/code.py b/app/apiserver/routers/code.py
index 21b8e6f..e134035 100644
--- a/app/apiserver/routers/code.py
+++ b/app/apiserver/routers/code.py
@@ -1,8 +1,6 @@
+from codepack import Default, Code, CodeSnapshot
 from fastapi import APIRouter
 from ..models.code import CodeID, CodeJSON, SnapshotJSON
-from codepack.config import Default
-from codepack import Code
-from codepack.snapshot import CodeSnapshot
 from ..dependencies import common
 
 
@@ -22,7 +20,7 @@ async def run(params: CodeJSON):
 
 @router.post('/run/id')
 async def run_by_id(params: CodeID):
-    storage_service = Default.get_storage_instance('code', 'storage_service')
+    storage_service = Default.get_service('code', 'storage_service')
     code = storage_service.load(params.id)
     common.supervisor.run_code(code=code, args=params.args, kwargs=params.kwargs)
     return {'serial_number': code.serial_number}
@@ -52,7 +50,7 @@ async def update(code: CodeJSON):
 
 @router.get('/remove/{id}')
 async def remove(id: str):
-    storage_service = Default.get_storage_instance('code', 'storage_service')
+    storage_service = Default.get_service('code', 'storage_service')
     storage_service.remove(id)
     return {'id': id}
 
@@ -65,18 +63,22 @@ async def load(id: str):
 
 @router.get('/state/{serial_number}')
 async def state(serial_number: str):
-    snapshot_service = Default.get_storage_instance('code_snapshot', 'snapshot_service')
-    ret = snapshot_service.load(serial_number=serial_number, projection={'state'})
-    if ret:
-        _state = ret['state']
+    snapshot_service = Default.get_service('code_snapshot', 'snapshot_service')
+    snapshot = snapshot_service.load(serial_number=serial_number, projection={'state', 'message'})
+    ret = {'serial_number': serial_number}
+    if snapshot:
+        _state = snapshot['state']
+        if snapshot['message']:
+            ret['message'] = snapshot['message']
     else:
         _state = 'UNKNOWN'
-    return {'seriual_number': serial_number, 'state': _state}
+    ret['state'] = _state
+    return ret
 
 
 @router.get('/result/{serial_number}')
 async def result(serial_number: str):
-    delivery_service = Default.get_storage_instance('delivery', 'delivery_service')
+    delivery_service = Default.get_service('delivery', 'delivery_service')
     tmp = delivery_service.check(serial_number=serial_number)
     if tmp:
         _result = delivery_service.receive(serial_number=serial_number)
diff --git a/app/apiserver/routers/codepack.py b/app/apiserver/routers/codepack.py
index 9a4986f..bf44720 100644
--- a/app/apiserver/routers/codepack.py
+++ b/app/apiserver/routers/codepack.py
@@ -1,9 +1,6 @@
+from codepack import Default, CodePack, CodePackSnapshot, ArgPack
 from fastapi import APIRouter
 from ..models.codepack import CodePackID, CodePackJSON, SnapshotJSON, IDPair
-from codepack.config import Default
-from codepack import CodePack
-from codepack.snapshot import CodePackSnapshot
-from codepack.argpack import ArgPack
 from ..dependencies import common
 
 
@@ -24,7 +21,7 @@ async def run(params: CodePackJSON):
 
 @router.post('/run/id')
 async def run_by_id(params: CodePackID):
-    storage_service = Default.get_storage_instance('codepack', 'storage_service')
+    storage_service = Default.get_service('codepack', 'storage_service')
     codepack = storage_service.load(params.id)
     argpack = ArgPack.from_json(params.argpack)
     common.supervisor.run_codepack(codepack=codepack, argpack=argpack)
@@ -33,9 +30,9 @@ async def run_by_id(params: CodePackID):
 
 @router.post('/run/id-pair')
 async def run_by_id_pair(params: IDPair):
-    codepack_storage_service = Default.get_storage_instance('codepack', 'storage_service')
+    codepack_storage_service = Default.get_service('codepack', 'storage_service')
     codepack = codepack_storage_service.load(params.codepack_id)
-    argpack_storage_service = Default.get_storage_instance('argpack', 'storage_service')
+    argpack_storage_service = Default.get_service('argpack', 'storage_service')
     argpack = argpack_storage_service.load(params.argpack_id)
     common.supervisor.run_codepack(codepack=codepack, argpack=argpack)
     return {'serial_number': codepack.serial_number}
@@ -66,34 +63,38 @@ async def update(codepack: CodePackJSON):
 
 @router.get('/remove/{id}')
 async def remove(id: str):
-    storage_service = Default.get_storage_instance('codepack', 'storage_service')
+    storage_service = Default.get_service('codepack', 'storage_service')
     storage_service.remove(id)
     return {'id': id}
 
 
 @router.get('/load/{id}')
 async def load(id: str):
-    storage_service = Default.get_storage_instance('codepack', 'storage_service')
+    storage_service = Default.get_service('codepack', 'storage_service')
     codepack = storage_service.load(id)
     return {'codepack': codepack.to_json()}
 
 
 @router.get('/state/{serial_number}')
 async def state(serial_number: str):
-    snapshot_service = Default.get_storage_instance('codepack_snapshot', 'snapshot_service')
+    snapshot_service = Default.get_service('codepack_snapshot', 'snapshot_service')
     tmp = snapshot_service.load(serial_number=serial_number)
+    ret = {'serial_number': serial_number}
     if tmp:
         codepack_snapshot = CodePackSnapshot.from_dict(tmp)
         codepack = CodePack.from_snapshot(codepack_snapshot)
         _state = codepack.get_state()
+        if _state == 'ERROR':
+            ret['message'] = codepack.get_message()
     else:
         _state = 'UNKNOWN'
-    return {'serial_number': serial_number, 'state': _state}
+    ret['state'] = _state
+    return ret
 
 
 @router.get('/result/{serial_number}')
 async def result(serial_number: str):
-    snapshot_service = Default.get_storage_instance('codepack_snapshot', 'snapshot_service')
+    snapshot_service = Default.get_service('codepack_snapshot', 'snapshot_service')
     snapshot = snapshot_service.load(serial_number=serial_number)
     _result = None
     if snapshot and snapshot['subscribe']:
diff --git a/app/apiserver/routers/scheduler.py b/app/apiserver/routers/scheduler.py
index 0646ad1..5c38290 100644
--- a/app/apiserver/routers/scheduler.py
+++ b/app/apiserver/routers/scheduler.py
@@ -1,10 +1,10 @@
+from codepack import Default, CodePack, CodePackSnapshot, ArgPack, Scheduler
 from fastapi import APIRouter
 from ..models.scheduler import CodePackIDJob, CodePackJSONJob, SnapshotJSONJob, IDPairJob
-from codepack.config import Default
-from codepack import CodePack
-from codepack.snapshot import CodePackSnapshot
-from codepack.argpack import ArgPack
 from ..dependencies import common
+import requests
+import json
+import os
 
 
 router = APIRouter(
@@ -16,43 +16,83 @@
 
 @router.post('/register')
 async def register(params: CodePackJSONJob):
-    codepack = CodePack.from_json(params.codepack)
-    argpack = ArgPack.from_json(params.argpack)
-    common.scheduler.add_codepack(codepack=codepack, argpack=argpack, job_id=params.job_id,
-                                  trigger=params.trigger, **params.trigger_config)
-    return {'serial_number': codepack.serial_number}
+    if isinstance(common.scheduler, str):
+        return redirect_to_remote_scheduler(requests.post, 'scheduler/register', data=json.dumps(params.dict()))
+    elif isinstance(common.scheduler, Scheduler):
+        codepack = CodePack.from_json(params.codepack)
+        argpack = ArgPack.from_json(params.argpack)
+        common.scheduler.add_codepack(codepack=codepack, argpack=argpack, job_id=params.job_id,
+                                      trigger=params.trigger, **params.trigger_config)
+        return {'serial_number': codepack.serial_number}
+    else:
+        raise TypeError(common.scheduler)
 
 
 @router.post('/register/id')
 async def register_by_id(params: CodePackIDJob):
-    storage_service = Default.get_storage_instance('codepack', 'storage_service')
-    codepack = storage_service.load(params.id)
-    argpack = ArgPack.from_json(params.argpack)
-    common.scheduler.add_codepack(codepack=codepack, argpack=argpack, job_id=params.job_id,
-                                  trigger=params.trigger, **params.trigger_config)
-    return {'serial_number': codepack.serial_number}
+    if isinstance(common.scheduler, str):
+        return redirect_to_remote_scheduler(requests.post, 'scheduler/register/id', data=json.dumps(params.dict()))
+    elif isinstance(common.scheduler, Scheduler):
+        storage_service = Default.get_service('codepack', 'storage_service')
+        codepack = storage_service.load(params.id)
+        argpack = ArgPack.from_json(params.argpack)
+        common.scheduler.add_codepack(codepack=codepack, argpack=argpack, job_id=params.job_id,
+                                      trigger=params.trigger, **params.trigger_config)
+        return {'serial_number': codepack.serial_number}
+    else:
+        raise TypeError(common.scheduler)
 
 
 @router.post('/register/id-pair')
 async def register_by_id_pair(params: IDPairJob):
-    codepack_storage_service = Default.get_storage_instance('codepack', 'storage_service')
-    codepack = codepack_storage_service.load(params.codepack_id)
-    argpack_storage_service = Default.get_storage_instance('argpack', 'storage_service')
-    argpack = argpack_storage_service.load(params.argpack_id)
-    common.scheduler.add_codepack(codepack=codepack, argpack=argpack, job_id=params.job_id,
-                                  trigger=params.trigger, **params.trigger_config)
-    return {'serial_number': codepack.serial_number}
+    if isinstance(common.scheduler, str):
+        return redirect_to_remote_scheduler(requests.post, 'scheduler/register/id-pair', data=json.dumps(params.dict()))
+    elif isinstance(common.scheduler, Scheduler):
+        codepack_storage_service = Default.get_service('codepack', 'storage_service')
+        codepack = codepack_storage_service.load(params.codepack_id)
+        argpack_storage_service = Default.get_service('argpack', 'storage_service')
+        argpack = argpack_storage_service.load(params.argpack_id)
+        common.scheduler.add_codepack(codepack=codepack, argpack=argpack, job_id=params.job_id,
+                                      trigger=params.trigger, **params.trigger_config)
+        return {'serial_number': codepack.serial_number}
+    else:
+        raise TypeError(common.scheduler)
 
 
 @router.post('/register/snapshot')
 async def register_by_snapshot(params: SnapshotJSONJob):
-    snapshot = CodePackSnapshot.from_json(params.snapshot)
-    common.scheduler.add_codepack(snapshot=snapshot, job_id=params.job_id,
-                                  trigger=params.trigger, **params.trigger_config)
-    return {'serial_number': snapshot.serial_number}
+    if isinstance(common.scheduler, str):
+        return redirect_to_remote_scheduler(requests.post, 'scheduler/register/snapshot', data=json.dumps(params.dict()))
+    elif isinstance(common.scheduler, Scheduler):
+        snapshot = CodePackSnapshot.from_json(params.snapshot)
+        common.scheduler.add_codepack(snapshot=snapshot, job_id=params.job_id,
+                                      trigger=params.trigger, **params.trigger_config)
+        return {'serial_number': snapshot.serial_number}
+    else:
+        raise TypeError(common.scheduler)
 
 
 @router.get('/unregister/{id}')
 async def unregister(id: str):
-    common.scheduler.remove_job(job_id=id)
-    return {'id': id}
+    if isinstance(common.scheduler, str):
+        return redirect_to_remote_scheduler(requests.get, 'scheduler/unregister/%s' % id)
+    elif isinstance(common.scheduler, Scheduler):
+        common.scheduler.remove_job(job_id=id)
+        return {'id': id}
+    else:
+        raise TypeError(common.scheduler)
+
+
+@router.get('/alive')
+async def alive():
+    if isinstance(common.scheduler, str):
+        return redirect_to_remote_scheduler(requests.get, 'scheduler/alive')
+    elif isinstance(common.scheduler, Scheduler):
+        is_alive = common.scheduler.is_running()
+        return {'alive': is_alive}
+    else:
+        raise TypeError(common.scheduler)
+
+
+def redirect_to_remote_scheduler(method, url, **kwargs):
+    return method(os.path.join(common.scheduler, url), **kwargs).json()
diff --git a/app/scheduler/__init__.py b/app/scheduler/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/scheduler/dependencies/__init__.py b/app/scheduler/dependencies/__init__.py
new file mode 100644
index 0000000..e0c935e
--- /dev/null
+++ b/app/scheduler/dependencies/__init__.py
@@ -0,0 +1,4 @@
+from codepack.utils import Common
+
+
+common = Common()
diff --git a/app/scheduler/main.py b/app/scheduler/main.py
new file mode 100644
index 0000000..4e0b734
--- /dev/null
+++ b/app/scheduler/main.py
@@ -0,0 +1,39 @@
+from codepack import Scheduler, Default
+from codepack.plugin.service import Service
+from fastapi import FastAPI, Request
+from .routers import scheduler
+from .dependencies import common
+
+
+app = FastAPI()
+app.include_router(scheduler.router)
+
+
+@app.middleware('http')
+async def add_process_time_header(request: Request, call_next):
+    response = await call_next(request)
+    response.headers['Access-Control-Allow_Origin'] = '*'
+    response.headers['Access-Control-Allow_Headers'] = 'Content-Type,Authorization'
+    response.headers['Access-Control-Allow-Methods'] = 'GET,PUT,POST,DELETE'
+    return response
+
+
+@app.on_event('startup')
+async def startup():
+    Default.init()
+    common.add(key='scheduler', value=Default.get_scheduler(), destroy=destroy_scheduler)
+    common.scheduler.start()
+
+
+@app.on_event('shutdown')
+async def shutdown():
+    for instance in Default.instances.values():
+        if isinstance(instance, Service):
+            instance.storage.close()
+    common.clear()
+
+
+def destroy_scheduler(x):
+    if isinstance(x, Scheduler):
+        if x.is_running():
+            x.stop()
diff --git a/app/scheduler/models/__init__.py b/app/scheduler/models/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/scheduler/models/scheduler.py b/app/scheduler/models/scheduler.py
new file mode 100644
index 0000000..0d249a1
--- /dev/null
+++ b/app/scheduler/models/scheduler.py
@@ -0,0 +1,26 @@
+from pydantic import BaseModel
+
+
+class Job(BaseModel):
+    job_id: str = None
+    trigger: str
+    trigger_config: dict
+
+
+class CodePackIDJob(Job):
+    id: str
+    argpack: str = '{}'
+
+
+class IDPairJob(Job):
+    codepack_id: str
+    argpack_id: str
+
+
+class CodePackJSONJob(Job):
+    codepack: str
+    argpack: str = '{}'
+
+
+class SnapshotJSONJob(Job):
+    snapshot: str
diff --git a/app/scheduler/routers/__init__.py b/app/scheduler/routers/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/scheduler/routers/scheduler.py b/app/scheduler/routers/scheduler.py
new file mode 100644
index 0000000..b9c21d8
--- /dev/null
+++ b/app/scheduler/routers/scheduler.py
@@ -0,0 +1,61 @@
+from codepack import CodePack, Default, CodePackSnapshot, ArgPack
+from fastapi import APIRouter
+from ..models.scheduler import CodePackIDJob, CodePackJSONJob, SnapshotJSONJob, IDPairJob
+from ..dependencies import common
+
+
+router = APIRouter(
+    prefix='/scheduler',
+    tags=['scheduler'],
+    responses={404: {'description': 'Not found'}},
+)
+
+
+@router.post('/register')
+async def register(params: CodePackJSONJob):
+    codepack = CodePack.from_json(params.codepack)
+    argpack = ArgPack.from_json(params.argpack)
+    common.scheduler.add_codepack(codepack=codepack, argpack=argpack, job_id=params.job_id,
+                                  trigger=params.trigger, **params.trigger_config)
+    return {'serial_number': codepack.serial_number}
+
+
+@router.post('/register/id')
+async def register_by_id(params: CodePackIDJob):
+    storage_service = Default.get_service('codepack', 'storage_service')
+    codepack = storage_service.load(params.id)
+    argpack = ArgPack.from_json(params.argpack)
+    common.scheduler.add_codepack(codepack=codepack, argpack=argpack, job_id=params.job_id,
+                                  trigger=params.trigger, **params.trigger_config)
+    return {'serial_number': codepack.serial_number}
+
+
+@router.post('/register/id-pair')
+async def register_by_id_pair(params: IDPairJob):
+    codepack_storage_service = Default.get_service('codepack', 'storage_service')
+    codepack = codepack_storage_service.load(params.codepack_id)
+    argpack_storage_service = Default.get_service('argpack', 'storage_service')
+    argpack = argpack_storage_service.load(params.argpack_id)
+    common.scheduler.add_codepack(codepack=codepack, argpack=argpack, job_id=params.job_id,
+                                  trigger=params.trigger, **params.trigger_config)
+    return {'serial_number': codepack.serial_number}
+
+
+@router.post('/register/snapshot')
+async def register_by_snapshot(params: SnapshotJSONJob):
+    snapshot = CodePackSnapshot.from_json(params.snapshot)
+    common.scheduler.add_codepack(snapshot=snapshot, job_id=params.job_id,
+                                  trigger=params.trigger, **params.trigger_config)
+    return {'serial_number': snapshot.serial_number}
+
+
+@router.get('/unregister/{id}')
+async def unregister(id: str):
+    common.scheduler.remove_job(job_id=id)
+    return {'id': id}
+
+
+@router.get('/alive')
+async def alive():
+    is_alive = common.scheduler.is_running()
+    return {'alive': is_alive}
diff --git a/app/worker/main.py b/app/worker/main.py
new file mode 100644
index 0000000..f20f644
--- /dev/null
+++ b/app/worker/main.py
@@ -0,0 +1,7 @@
+from codepack import Default
+
+
+if __name__ == '__main__':
+    worker = Default.get_employee('worker')
+    worker.start()
+    worker.stop()
diff --git a/codepack/__init__.py b/codepack/__init__.py
index 13fd670..e8f9c59 100644
--- a/codepack/__init__.py
+++ b/codepack/__init__.py
@@ -1,2 +1,30 @@
-from codepack.code import Code
-from codepack.codepack import CodePack
+from .code import Code
+from .codepack import CodePack
+
+from .plugin.worker import Worker
+from .plugin.supervisor import Supervisor
+from .plugin.docker_manager import DockerManager
+from .plugin.interpreter_manager import InterpreterManager
+from .plugin.dependency_bag import DependencyBag
+from .plugin.dependency_monitor import DependencyMonitor
+from .plugin.storage_service import StorageService
+from .plugin.snapshot_service import SnapshotService
+from .plugin.delivery_service import DeliveryService
+from .plugin.callback_service import CallbackService
+from .plugin.scheduler import Scheduler
+
+from .snapshot.state import State
+from .snapshot.snapshot import Snapshot
+from .snapshot.code_snapshot import CodeSnapshot
+from .snapshot.codepack_snapshot import CodePackSnapshot
+
+from .dependency.dependency import Dependency
+from .delivery.delivery import Delivery
+from .callback.callback import Callback
+
+from codepack.arg import Arg
+from codepack.argpack import ArgPack
+
+from .config.config import Config
+from .config.default import Default
+from .config.alias import Alias
diff --git a/codepack/argpack/arg.py b/codepack/arg.py
similarity index 63%
rename from codepack/argpack/arg.py
rename to codepack/arg.py
index c9b8b6a..1306fe0 100644
--- a/codepack/argpack/arg.py
+++ b/codepack/arg.py
@@ -1,8 +1,12 @@
-from codepack.storage import Storable
+from codepack.storage.storable import Storable
+from typing import Optional, Any, TypeVar, Iterator
+
+
+Code = TypeVar('Code', bound='codepack.code.Code')
 
 
 class Arg(Storable):
-    def __init__(self, code=None, kwargs=None):
+    def __init__(self, code: Optional[Code] = None, kwargs: Optional[dict] = None) -> None:
         Storable.__init__(self)
         self.kwargs = dict()
         if kwargs:
@@ -12,39 +16,39 @@ def __init__(self, code=None, kwargs=None):
             self.kwargs = self.extract(code)
 
     @staticmethod
-    def extract(code):
+    def extract(code: Optional[Code]) -> dict:
         ret = dict()
         for arg, value in code.get_args().items():
             if arg not in code.dependency.get_args().keys():
                 ret[arg] = value
         return ret
 
-    def __getitem__(self, item):
+    def __getitem__(self, item: str) -> Any:
         return self.kwargs[item]
 
-    def __setitem__(self, key, value):
+    def __setitem__(self, key: str, value: Any) -> None:
         self.kwargs[key] = value
 
-    def __call__(self, **kwargs):
+    def __call__(self, **kwargs: Any) -> None:
         for arg, value in kwargs.items():
             if arg not in self.kwargs:
                 raise TypeError(arg)
             self.kwargs[arg] = value
 
-    def to_dict(self):
+    def to_dict(self) -> dict:
         return self.kwargs
 
     @classmethod
-    def from_dict(cls, d):
+    def from_dict(cls, d: dict) -> 'Arg':
         return cls(kwargs=d)
 
-    def __getattr__(self, item):
+    def __getattr__(self, item: str) -> Any:
         return getattr(self.kwargs, item)
 
-    def __iter__(self):
+    def __iter__(self) -> Iterator[str]:
         return self.kwargs.__iter__()
 
-    def __str__(self):
+    def __str__(self) -> str:
         ret = '%s(' % self.__class__.__name__
         for i, (arg, value) in enumerate(self.kwargs.items()):
             if i:
@@ -53,5 +57,5 @@ def __str__(self):
         ret += ')'
         return ret
 
-    def __repr__(self):
+    def __repr__(self) -> str:
         return self.__str__()  # pragma: no cover
diff --git a/codepack/argpack/argpack.py b/codepack/argpack.py
similarity index 69%
rename from codepack/argpack/argpack.py
rename to codepack/argpack.py
index 665f2a9..76e5544 100644
--- a/codepack/argpack/argpack.py
+++ b/codepack/argpack.py
@@ -1,9 +1,14 @@
-from codepack.argpack import Arg
-from codepack.storage import Storable
+from codepack.arg import Arg
+from codepack.storage.storable import Storable
+from typing import Optional, TypeVar, Union, Iterator
+
+
+CodePack = TypeVar('CodePack', bound='codepack.codepack.CodePack')
 
 
 class ArgPack(Storable):
-    def __init__(self, codepack=None, id=None, args=None):
+    def __init__(self, codepack: Optional[CodePack] = None, id: Optional[str] = None,
+                 args: Optional[dict] = None) -> None:
         _id = None
         if codepack:
             _id = codepack.id
@@ -20,7 +25,7 @@ def __init__(self, codepack=None, id=None, args=None):
             self.args = dict()
 
     @staticmethod
-    def extract(codepack):
+    def extract(codepack: CodePack) -> dict:
         ret = dict()
         stack = list()
         for root in codepack.roots:
@@ -33,13 +38,13 @@ def extract(codepack):
                     stack.append(c)
         return ret
 
-    def __getitem__(self, item):
+    def __getitem__(self, item: str) -> Arg:
         return self.args[item]
 
-    def __setitem__(self, key, value):
+    def __setitem__(self, key: str, value: Union[Arg, dict]) -> None:
         self.args[key] = value
 
-    def to_dict(self):
+    def to_dict(self) -> dict:
         ret = dict()
         for id, arg in self.args.items():
             ret[id] = arg.to_dict()
@@ -47,7 +52,7 @@ def to_dict(self):
         return ret
 
     @classmethod
-    def from_dict(cls, d):
+    def from_dict(cls, d: dict) -> 'ArgPack':
         args = dict()
         id = None
         for k, v in d.items():
@@ -57,13 +62,13 @@ def from_dict(cls, d):
                 args[k] = v
         return cls(id=id, args=args)
 
-    def __getattr__(self, item):
+    def __getattr__(self, item: str) -> Arg:
         return getattr(self.args, item)
 
-    def __iter__(self):
+    def __iter__(self) -> Iterator[str]:
         return self.args.__iter__()
 
-    def __str__(self):
+    def __str__(self) -> str:
         ret = '%s(id: %s, args: {' % (self.__class__.__name__, self.id)
         for i, (id, arg) in enumerate(self.args.items()):
             if i:
@@ -72,5 +77,5 @@ def __str__(self):
         ret += '})'
         return ret
 
-    def __repr__(self):
+    def __repr__(self) -> str:
         return self.__str__()  # pragma: no cover
diff --git a/codepack/argpack/__init__.py b/codepack/argpack/__init__.py
deleted file mode 100644
index 2987f01..0000000
--- a/codepack/argpack/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-from codepack.argpack.arg import Arg
-from codepack.argpack.argpack import ArgPack
diff --git a/codepack/base/__init__.py b/codepack/base/__init__.py
index 74f26c9..e69de29 100644
--- a/codepack/base/__init__.py
+++ b/codepack/base/__init__.py
@@ -1,2 +0,0 @@
-from codepack.base.code_base import CodeBase
-from codepack.base.codepack_base import CodePackBase
diff --git a/codepack/base/code_base.py b/codepack/base/code_base.py
index d4decfb..d4f8ad2 100644
--- a/codepack/base/code_base.py
+++ b/codepack/base/code_base.py
@@ -1,9 +1,61 @@
-from codepack.storage import Storable
-from codepack.snapshot import Snapshotable
+from codepack.storage.storable import Storable
+from codepack.snapshot.snapshotable import Snapshotable
+from collections.abc import Callable
+from collections import OrderedDict
 import abc
+import re
+import ast
+import inspect
+import dill
+from typing import Optional
 
 
 class CodeBase(Storable, Snapshotable, metaclass=abc.ABCMeta):
-    @abc.abstractmethod
-    def __init__(self, id=None, serial_number=None):
+    def __init__(self, id: Optional[str] = None, serial_number: Optional[str] = None) -> None:
         Storable.__init__(self, id=id, serial_number=serial_number)
+        Snapshotable.__init__(self)
+        self.function = None
+        self.source = None
+        self.description = None
+
+    @staticmethod
+    def get_source(function: Callable) -> str:
+        assert isinstance(function, Callable), "'function' should be an instance of Callable"
+        assert function.__name__ != '<lambda>', "Invalid function '<lambda>'"
+        assert hasattr(function, '__code__'), "'function' should have an attribute '__code__'"
+        assert getattr(function, '__code__').co_filename != '<string>', "'function' should not be defined in <string>"
+        source = None
+        for test in [inspect.getsource, dill.source.getsource]:
+            try:
+                source = test(function)
+            except Exception:  # pragma: no cover
+                pass  # pragma: no cover
+            if source is not None:
+                break
+        return source
+
+    @staticmethod
+    def get_function(source: str) -> Callable:
+        pat = re.compile('^(\\s*def\\s.+[(].*[)].*[:])|(\\s*async\\s+def\\s.+[(].*[)].*[:])')
+        assert pat.match(source), "'source' is not a function"
+        tree = ast.parse(source, mode='exec')
+        n_function = sum(isinstance(exp, ast.FunctionDef) for exp in tree.body)
+        # needs to count all other instances, and assert that there is only one FunctionDef
+        assert n_function == 1, "'source' should contain only one function."
+        namespace = dict()
+        # code = compile(tree, filename='blah', mode='exec')
+        exec(source, namespace)
+        return namespace[getattr(tree.body[0], 'name')]
+
+    @staticmethod
+    def get_args(function: Callable) -> OrderedDict:
+        ret = OrderedDict()
+        argspec = inspect.getfullargspec(function)
+        args = argspec.args
+        defaults = dict(zip(args[-len(argspec.defaults):], argspec.defaults)) if argspec.defaults else dict()
+        for arg in args:
+            if arg in defaults:
+                ret[arg] = defaults[arg]
+            else:
+                ret[arg] = None
+        return ret
diff --git a/codepack/base/codepack_base.py b/codepack/base/codepack_base.py
index 4228c78..e27be21 100644
--- a/codepack/base/codepack_base.py
+++ b/codepack/base/codepack_base.py
@@ -1,9 +1,10 @@
-from codepack.storage import Storable
-from codepack.snapshot import Snapshotable
+from codepack.storage.storable import Storable
+from codepack.snapshot.snapshotable import Snapshotable
 import abc
+from typing import Optional
 
 
 class CodePackBase(Storable, Snapshotable, metaclass=abc.ABCMeta):
     @abc.abstractmethod
-    def __init__(self, id=None, serial_number=None):
+    def __init__(self, id: Optional[str] = None, serial_number: Optional[str] = None) -> None:
         Storable.__init__(self, id=id, serial_number=serial_number)
diff --git a/codepack/callback/__init__.py b/codepack/callback/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/codepack/callback/callback.py b/codepack/callback/callback.py
new file mode 100644
index 0000000..7086295
--- /dev/null
+++ b/codepack/callback/callback.py
@@ -0,0 +1,45 @@
+from codepack.storage.storable import Storable
+from codepack.base.code_base import CodeBase
+from functools import partial
+from collections.abc import Callable
+from typing import Optional, Any
+
+
+class Callback(Storable, Callable):
+    def __init__(self, function: Callable, context: Optional[dict] = None) -> None:
+        Storable.__init__(self)
+        Callable.__init__(self)
+        self.function = None
+        self.context = None
+        self.set_function(function=function, context=context)
+
+    def set_function(self, function: Callable, context: Optional[dict] = None) -> None:
+        if context is None:
+            self.context = dict()
+        else:
+            self.context = context
+        if isinstance(function, partial):
+            self.function = function.func
+            for i, k in enumerate(CodeBase.get_args(self.function).keys()):
+                if i >= len(function.args):
+                    break
+                if k not in self.context:
+                    self.context[k] = function.args[i]
+            for k, v in function.keywords.items():
+                if k not in self.context:
+                    self.context[k] = v
+        elif isinstance(function, Callable):
+            self.function = function
+        else:
+            raise TypeError(type(function))
+        self.id = self.function.__name__
+
+    def to_dict(self) -> dict:
+        return {'source': CodeBase.get_source(self.function), 'context': self.context}
+
+    @classmethod
+    def from_dict(cls, d: dict) -> 'Callback':
+        return cls(function=CodeBase.get_function(d['source']), context=d['context'])
+
+    def __call__(self, *args: Any, **kwargs: Any) -> Any:
+        return self.function(*args, **kwargs, **self.context)
diff --git a/codepack/callback/functions.py b/codepack/callback/functions.py
new file mode 100644
index 0000000..7c235ac
--- /dev/null
+++ b/codepack/callback/functions.py
@@ -0,0 +1,8 @@
+def inform_supervisor_of_termination(x, supervisor):
+    import requests
+    from codepack.plugin.supervisor import Supervisor
+    if x['state'] == 'TERMINATED':
+        if isinstance(supervisor, str):
+            requests.get(supervisor + '/organize/%s' % x['serial_number'])
+        elif isinstance(supervisor, Supervisor):
+            supervisor.organize(x['serial_number'])
diff --git a/codepack/code.py b/codepack/code.py
index 36f93d0..fd0f2e6 100644
--- a/codepack/code.py
+++ b/codepack/code.py
@@ -1,30 +1,39 @@
-import inspect
+from codepack.config.default import Default
+from codepack.base.code_base import CodeBase
+from codepack.dependency.dependency import Dependency
+from codepack.plugin.dependency_bag import DependencyBag
+from codepack.callback.callback import Callback
 from collections.abc import Iterable, Callable
-import dill
-from codepack.config import Default
-from codepack.base import CodeBase
-import re
-import ast
-from collections import OrderedDict
-from datetime import datetime
-from codepack.dependency import Dependency, DependencyManager
-from codepack.snapshot import CodeSnapshot
+from functools import partial
+from typing import Any, TypeVar, Union, Optional
+
+
+CodeSnapshot = TypeVar('CodeSnapshot', bound='codepack.snapshot.code_snapshot.CodeSnapshot')
+DeliveryService = TypeVar('DeliveryService', bound='codepack.plugin.delivery_service.DeliveryService')
+SnapshotService = TypeVar('SnapshotService', bound='codepack.plugin.snapshot_service.SnapshotService')
+StorageService = TypeVar('StorageService', bound='codepack.plugin.storage_service.StorageService')
+State = TypeVar('State', bound='codepack.snapshot.state.State')
 
 
 class Code(CodeBase):
-    def __init__(self, function=None, source=None, id=None, serial_number=None, dependency=None,
-                 config_path=None, delivery_service=None, snapshot_service=None, storage_service=None,
-                 state=None, callback=None):
+    def __init__(self, function: Optional[Callable] = None, source: Optional[str] = None,
+                 id: Optional[str] = None, serial_number: Optional[str] = None,
+                 dependency: Optional[Union[Dependency, dict, Iterable]] = None, config_path: Optional[str] = None,
+                 delivery_service: Optional[DeliveryService] = None,
+                 snapshot_service: Optional[SnapshotService] = None,
+                 storage_service: Optional[StorageService] = None,
+                 state: Optional[Union[State, str]] = None, callback: Optional[Union[list, Callable, Callback]] = None,
+                 env: Optional[str] = None, image: Optional[str] = None, owner: Optional[str] = None) -> None:
         super().__init__(id=id, serial_number=serial_number)
-        self.function = None
-        self.source = None
-        self.description = None
         self.parents = None
         self.children = None
         self.dependency = None
         self.config_path = None
         self.service = None
-        self.callback = None
+        self.callbacks = dict()
+        self.env = None
+        self.image = None
+        self.owner = None
         self.init_service(delivery_service=delivery_service,
                           snapshot_service=snapshot_service,
                           storage_service=storage_service,
@@ -34,63 +43,89 @@ def __init__(self, function=None, source=None, id=None, serial_number=None, depe
             self.id = self.function.__name__
         self.init_linkage()
         self.init_dependency(dependency=dependency)
-        self.register(callback=callback)
+        self.register_callback(callback=callback)
+        self._set_str_attr(key='env', value=env)
+        self._set_str_attr(key='image', value=image)
+        self._set_str_attr(key='owner', value=owner)
         self.update_state(state)
 
-    def init_linkage(self):
+    def init_linkage(self) -> None:
         self.parents = dict()
         self.children = dict()
 
-    def init_dependency(self, dependency=None):
-        self.dependency = DependencyManager(code=self)
+    def init_dependency(self, dependency: Optional[Union[Dependency, dict, Iterable]] = None) -> None:
+        self.dependency = DependencyBag(code=self)
         if dependency:
             self.add_dependency(dependency=dependency)
 
-    def init_service(self, delivery_service=None, snapshot_service=None, storage_service=None, config_path=None):
+    def init_service(self, delivery_service: Optional[DeliveryService] = None,
+                     snapshot_service: Optional[DeliveryService] = None,
+                     storage_service: Optional[DeliveryService] = None, config_path: Optional[str] = None) -> None:
         self.service = dict()
         self.service['delivery'] =\
-            delivery_service if delivery_service else Default.get_storage_instance('delivery', 'delivery_service',
-                                                                                   config_path=config_path)
+            delivery_service if delivery_service else Default.get_service('delivery', 'delivery_service',
+                                                                          config_path=config_path)
         self.service['snapshot'] =\
-            snapshot_service if snapshot_service else Default.get_storage_instance('code_snapshot', 'snapshot_service',
-                                                                                   config_path=config_path)
+            snapshot_service if snapshot_service else Default.get_service('code_snapshot', 'snapshot_service',
+                                                                          config_path=config_path)
         self.service['storage'] =\
-            storage_service if storage_service else Default.get_storage_instance('code', 'storage_service',
-                                                                                 config_path=config_path)
-
-    def register(self, callback):
-        self.callback = callback
-
-    @staticmethod
-    def get_source(function):
-        assert isinstance(function, Callable), "'function' should be an instance of Callable"
-        assert function.__name__ != '<lambda>', "Invalid function '<lambda>'"
-        assert hasattr(function, '__code__'), "'function' should have an attribute '__code__'"
-        assert function.__code__.co_filename != '<string>', "'function' should not be defined in <string>"
-        source = None
-        for test in [inspect.getsource, dill.source.getsource]:
-            try:
-                source = test(function)
-            except Exception:  # pragma: no cover
-                pass  # pragma: no cover
-            if source is not None:
-                break
-        return source
-
-    @staticmethod
-    def get_function(source):
-        pat = re.compile('^(\\s*def\\s.+[(].*[)].*[:])|(\\s*async\\s+def\\s.+[(].*[)].*[:])')
-        assert pat.match(source), "'source' is not a function"
-        tree = ast.parse(source, mode='exec')
-        n_function = sum(isinstance(exp, ast.FunctionDef) for exp in tree.body)
-        # needs to count all other instances, and assert that there is only one FunctionDef
-        assert n_function == 1, "'source' should contain only one function."
-        namespace = dict()
-        # code = compile(tree, filename='blah', mode='exec')
-        exec(source, namespace)
-        return namespace[tree.body[0].name]
+            storage_service if storage_service else Default.get_service('code', 'storage_service',
+                                                                        config_path=config_path)
+
+    def _set_str_attr(self, key: str, value: str) -> None:
+        if value is None or value == 'None' or value == 'null':
+            _value = None
+        else:
+            _value = value
+        setattr(self, key, _value)
+
+    def register_callback(self, callback: Union[list, Callable, Callback],
+                          name: Optional[Union[list, str]] = None) -> None:
+        if isinstance(callback, list):
+            if isinstance(name, list):
+                if len(callback) != len(name):
+                    raise IndexError('len(callback) != len(name): %s != %s' % (len(callback), len(name)))
+                else:
+                    for c, n in zip(callback, name):
+                        self.register_callback(callback=c, name=n)
+            elif isinstance(name, str):
+                if len(callback) == 1:
+                    self.register_callback(callback=callback[0], name=name)
+                else:
+                    raise IndexError('len(callback) != len(name): %s != 1' % len(callback))
+            elif name is None:
+                for c in callback:
+                    self.register_callback(callback=c)
+            else:
+                raise TypeError(type(name))
+        elif isinstance(callback, Callable):
+            if isinstance(name, list):
+                _name = name[0]
+            elif isinstance(name, str):
+                _name = name
+            elif name is None:
+                if isinstance(callback, partial):
+                    _name = callback.func.__name__
+                elif isinstance(callback, Callback):
+                    _name = callback.function.__name__
+                else:
+                    _name = callback.__name__
+            else:
+                raise TypeError(type(name))
+            self.callbacks[_name] = callback
+        elif callback is None:
+            pass
+        else:
+            raise TypeError(type(callback))
+
+    def run_callback(self, state: Optional[Union[State, str]] = None, message: Optional[str] = None) -> None:
+        for callback in self.callbacks.values():
+            d = {'serial_number': self.serial_number, 'state': state if state else self.get_state()}
+            if message:
+                d['message'] = message
+            callback(d)
     
-    def set_function(self, function=None, source=None):
+    def set_function(self, function: Optional[Callable] = None, source: Optional[str] = None) -> None:
         if source:
             source = source.strip()
             self.function = self.get_function(source)
@@ -106,19 +141,19 @@ def set_function(self, function=None, source=None):
             raise AssertionError("either 'function' or 'source' should not be None")
         self.description = self.function.__doc__.strip() if self.function.__doc__ is not None else str()
 
-    def link(self, other):
+    def link(self, other: 'Code') -> None:
         self.children[other.id] = other
         other.parents[self.id] = self
         if self.serial_number not in other.dependency:
             other.add_dependency(dependency=Dependency(code=other, serial_number=self.serial_number, id=self.id))
 
-    def unlink(self, other):
+    def unlink(self, other: 'Code') -> None:
         other.parents.pop(self.id, None)
         self.children.pop(other.id, None)
         if self.serial_number in other.dependency:
             other.remove_dependency(self.serial_number)
 
-    def __rshift__(self, other):
+    def __rshift__(self, other: Union['Code', Iterable]) -> Union['Code', Iterable]:
         if isinstance(other, self.__class__):
             self.link(other)
         elif isinstance(other, Iterable):
@@ -128,7 +163,7 @@ def __rshift__(self, other):
             raise TypeError(type(other))  # pragma: no cover
         return other
 
-    def __floordiv__(self, other):
+    def __floordiv__(self, other: Union['Code', Iterable]) -> Union['Code', Iterable]:
         if isinstance(other, self.__class__):
             self.unlink(other)
         elif isinstance(other, Iterable):
@@ -138,7 +173,7 @@ def __floordiv__(self, other):
             raise TypeError(type(other))  # pragma: no cover
         return other
 
-    def update_serial_number(self, serial_number):
+    def update_serial_number(self, serial_number: str) -> None:
         for c in self.children.values():
             if self.serial_number in c.dependency:
                 d = c.dependency[self.serial_number]
@@ -147,19 +182,10 @@ def update_serial_number(self, serial_number):
                 c.add_dependency(d)
         self.serial_number = serial_number
 
-    def get_args(self):
-        ret = OrderedDict()
-        argspec = inspect.getfullargspec(self.function)
-        args = argspec.args
-        defaults = dict(zip(args[-len(argspec.defaults):], argspec.defaults)) if argspec.defaults else dict()
-        for arg in args:
-            if arg in defaults:
-                ret[arg] = defaults[arg]
-            else:
-                ret[arg] = None
-        return ret
+    def get_args(self) -> dict:
+        return super().get_args(function=self.function)
 
-    def print_args(self):
+    def print_args(self) -> str:
         ret = '('
         for i, (arg, value) in enumerate(self.get_args().items()):
             if i:
@@ -170,105 +196,125 @@ def print_args(self):
         ret += ')'
         return ret
 
-    def get_info(self, state=True):
-        ret = '%s(id: %s, function: %s, args: %s, receive: %s'
+    @classmethod
+    def blueprint(cls, s: str) -> str:
+        ret = 'Code(id: {id}, function: {function}, args: {args}, receive: {receive}'
+        for additional_item in ['env', 'image', 'owner', 'state']:
+            if ', %s:' % additional_item in s:
+                ret += ', %s: {%s}' % (additional_item, additional_item)
+        ret += ')'
+        return ret
+
+    def get_info(self, state: bool = True) -> str:
+        ret = '%s(id: %s, function: %s, args: %s, receive: %s' % (self.__class__.__name__,
+                                                                  self.id, self.function.__name__,
+                                                                  self.print_args(), self.dependency.get_args())
+        for additional_item in ['env', 'image', 'owner']:
+            item = getattr(self, additional_item)
+            if item:
+                ret += ', %s: %s' % (additional_item, item)
         if state:
-            ret += ', state: %s)'
-            return ret % (self.__class__.__name__, self.id, self.function.__name__,
-                          self.print_args(),
-                          self.dependency.get_args(),
-                          self.get_state())
-        else:
-            ret += ')'
-            return ret % (self.__class__.__name__, self.id, self.function.__name__,
-                          self.print_args(),
-                          self.dependency.get_args())
+            ret += ', state: %s' % self.get_state()
+        ret += ')'
+        return ret
 
-    def __str__(self):
+    def __str__(self) -> str:
         return self.get_info(state=False)  # pragma: no cover
 
-    def __repr__(self):
+    def __repr__(self) -> str:
         return self.__str__()  # pragma: no cover
 
-    def update_state(self, state, timestamp=None, args=None, kwargs=None):
+    def update_state(self, state: Union[State, str], timestamp: Optional[float] = None,
+                     args: Optional[tuple] = None, kwargs: Optional[dict] = None, message: str = '') -> None:
         if state:
-            snapshot = self.to_snapshot(args=args, kwargs=kwargs, timestamp=timestamp)
+            snapshot = self.to_snapshot(args=args, kwargs=kwargs, timestamp=timestamp, message=message)
             snapshot['state'] = state
             self.service['snapshot'].save(snapshot)
-            if self.callback:
-                self.callback({'serial_number': self.serial_number, 'state': state})
+            self.run_callback(state=state, message=message)
 
-    def get_state(self):
+    def get_state(self) -> str:
         ret = self.service['snapshot'].load(serial_number=self.serial_number, projection={'state'})
         if ret:
             return ret['state']
         else:
             return 'UNKNOWN'
 
-    def send_result(self, item, timestamp=None):
+    def get_message(self) -> str:
+        ret = self.service['snapshot'].load(serial_number=self.serial_number, projection={'message'})
+        if ret:
+            return ret['message']
+        else:
+            return ''
+
+    def send_result(self, item: Any, timestamp: Optional[float] = None) -> None:
         self.service['delivery'].send(id=self.id, serial_number=self.serial_number, item=item, timestamp=timestamp)
 
-    def get_result(self, serial_number=None):
+    def get_result(self, serial_number: Optional[str] = None) -> Any:
         serial_number = serial_number if serial_number else self.serial_number
         return self.service['delivery'].receive(serial_number=serial_number)
 
-    def save(self, update=False):
+    def save(self, update: bool = False) -> None:
         self.service['storage'].save(item=self, update=update)
 
-    def receive(self, arg):
+    def receive(self, arg: str) -> Dependency:
         self.assert_arg(arg)
         return Dependency(code=self, arg=arg)
 
-    def assert_arg(self, arg):
+    def assert_arg(self, arg: str) -> None:
         if arg and arg not in self.get_args():
             raise AssertionError("'%s' is not an argument of %s" % (arg, self.function))
 
-    def add_dependency(self, dependency):
+    def add_dependency(self, dependency: Union[Dependency, dict, Iterable]) -> None:
         self.dependency.add(dependency=dependency)
 
-    def remove_dependency(self, serial_number):
+    def remove_dependency(self, serial_number: str) -> None:
         self.dependency.remove(serial_number)
 
-    def __call__(self, *args, **kwargs):
+    def check_dependency(self) -> str:
+        return self.dependency.get_state()
+
+    def _run(self, *args: Any, **kwargs: Any) -> Any:
+        for dependency in self.dependency.values():
+            if dependency.arg and dependency.arg not in kwargs:
+                kwargs[dependency.arg] = self.get_result(serial_number=dependency.serial_number)
+        ret = self.function(*args, **kwargs)
+        self.send_result(item=ret)
+        return ret
+
+    def __call__(self, *args: Any, **kwargs: Any) -> Any:
+        ret = None
         try:
-            dependency_state = self.dependency.get_state()
-            if dependency_state == 'RESOLVED':
-                for dependency in self.dependency.values():
-                    if dependency.arg and dependency.arg not in kwargs:
-                        kwargs[dependency.arg] = self.get_result(serial_number=dependency.serial_number)
+            state = self.check_dependency()
+            self.update_state(state, args=args, kwargs=kwargs)
+            if state == 'READY':
                 self.update_state('RUNNING', args=args, kwargs=kwargs)
-                ret = self.function(*args, **kwargs)
-                now = datetime.now().timestamp()
-                self.send_result(item=ret, timestamp=now)
-                self.update_state('TERMINATED', args=args, kwargs=kwargs, timestamp=now)
-                return ret
-            elif dependency_state == 'PENDING':
-                self.update_state('WAITING', args=args, kwargs=kwargs)
-                return None
-            elif dependency_state == 'ERROR':
-                self.update_state('ERROR', args=args, kwargs=kwargs)
-                return None
-            else:
-                raise NotImplementedError(dependency_state)  # pragma: no cover
+                ret = self._run(*args, **kwargs)
+                self.update_state('TERMINATED', args=args, kwargs=kwargs)
         except Exception as e:
-            self.update_state('ERROR', args=args, kwargs=kwargs)
+            self.update_state('ERROR', args=args, kwargs=kwargs, message=str(e))
             raise e
+        return ret
 
-    def to_dict(self):
+    def to_dict(self) -> dict:
         d = dict()
         d['_id'] = self.id
         d['source'] = self.source
         d['description'] = self.description
+        d['env'] = self.env
+        d['image'] = self.image
+        d['owner'] = self.owner
         return d
 
     @classmethod
-    def from_dict(cls, d):
-        return cls(id=d['_id'], source=d['source'])
+    def from_dict(cls, d: dict) -> 'Code':
+        return cls(id=d['_id'], source=d['source'],
+                   env=d.get('env', None), image=d.get('image', None), owner=d.get('owner', None))
 
-    def to_snapshot(self, *args, **kwargs):
-        return CodeSnapshot(self, *args, **kwargs)
+    def to_snapshot(self, *args: Any, **kwargs: Any) -> CodeSnapshot:
+        return self.service['snapshot'].convert_to_snapshot(self, *args, **kwargs)
 
     @classmethod
-    def from_snapshot(cls, snapshot):
+    def from_snapshot(cls, snapshot: CodeSnapshot) -> 'Code':
         return cls(id=snapshot['id'], serial_number=snapshot['serial_number'],
-                   state=snapshot['state'], dependency=snapshot['dependency'], source=snapshot['source'])
+                   state=snapshot['state'], dependency=snapshot['dependency'], source=snapshot['source'],
+                   env=snapshot['env'], image=snapshot['image'], owner=snapshot['owner'])
diff --git a/codepack/codepack.py b/codepack/codepack.py
index 8a94aff..c694494 100644
--- a/codepack/codepack.py
+++ b/codepack/codepack.py
@@ -1,66 +1,95 @@
-from codepack.base import CodeBase, CodePackBase
-from codepack import Code
-from queue import Queue
-from codepack.config import Default
-from codepack.snapshot import State
+from codepack.code import Code
+from codepack.base.codepack_base import CodePackBase
+from codepack.argpack import ArgPack
+from codepack.config.default import Default
+from codepack.snapshot.state import State
 from parse import compile as parser
 from ast import literal_eval
-from codepack.snapshot import CodePackSnapshot
-from codepack.argpack import ArgPack
+from queue import Queue
+from typing import Any, TypeVar, Optional, Union
+
+
+CodePackSnapshot = TypeVar('CodePackSnapshot', bound='codepack.snapshot.codepack_snapshot.CodePackSnapshot')
+SnapshotService = TypeVar('SnapshotService', bound='codepack.plugin.snapshot_service.SnapshotService')
+StorageService = TypeVar('StorageService', bound='codepack.plugin.storage_service.StorageService')
 
 
 class CodePack(CodePackBase):
-    def __init__(self, id, code, subscribe=None, serial_number=None, config_path=None,
-                 snapshot_service=None, storage_service=None, argpack_service=None):
+    def __init__(self, id: str, code: Code, subscribe: Optional[Union[Code, str]] = None,
+                 serial_number: Optional[str] = None, config_path: Optional[str] = None,
+                 snapshot_service: Optional[SnapshotService] = None,
+                 storage_service: Optional[StorageService] = None,
+                 argpack_service: Optional[StorageService] = None,
+                 owner: Optional[str] = None) -> None:
         super().__init__(id=id, serial_number=serial_number)
         self.root = None
         self.roots = None
+        self.subscribe = None
         self.service = None
-        self.set_root(code)
-        if isinstance(subscribe, CodeBase):
-            self.subscribe = subscribe.id
-        elif isinstance(subscribe, str):
-            self.subscribe = subscribe
-        else:
-            self.subscribe = None
+        self.owner = None
+        self._set_root(code)
         self.codes = dict()
         self.init()
+        self.init_subscription(subscribe=subscribe)
         self.init_service(snapshot_service=snapshot_service,
                           storage_service=storage_service,
                           argpack_service=argpack_service,
                           config_path=config_path)
+        self._set_str_attr(key='owner', value=owner)
 
-    def init(self):
-        self.roots = self.get_roots()
+    def init(self) -> None:
+        self.roots = self._get_roots()
 
-    def init_service(self, snapshot_service=None, storage_service=None, argpack_service=None, config_path=None):
+    def init_service(self, snapshot_service: Optional[SnapshotService] = None,
+                     storage_service: Optional[StorageService] = None,
+                     argpack_service: Optional[StorageService] = None, config_path: Optional[str] = None) -> None:
         self.service = dict()
         self.service['snapshot'] =\
-            snapshot_service if snapshot_service else Default.get_storage_instance('codepack_snapshot', 'snapshot_service',
-                                                                                   config_path=config_path)
+            snapshot_service if snapshot_service else Default.get_service('codepack_snapshot', 'snapshot_service',
+                                                                          config_path=config_path)
         self.service['storage'] =\
-            storage_service if storage_service else Default.get_storage_instance('codepack', 'storage_service',
-                                                                                 config_path=config_path)
+            storage_service if storage_service else Default.get_service('codepack', 'storage_service',
+                                                                        config_path=config_path)
         self.service['argpack'] = \
-            argpack_service if argpack_service else Default.get_storage_instance('argpack', 'storage_service',
-                                                                                 config_path=config_path)
+            argpack_service if argpack_service else Default.get_service('argpack', 'storage_service',
+                                                                        config_path=config_path)
 
-    def make_argpack(self):
+    def init_subscription(self, subscribe: Optional[Union[Code, str]] = None) -> None:
+        if isinstance(subscribe, Code):
+            self.subscribe = subscribe.id
+        elif isinstance(subscribe, str):
+            self.subscribe = subscribe
+        else:
+            self.subscribe = None
+
+    def _set_str_attr(self, key: str, value: str) -> None:
+        if value is None or value == 'None' or value == 'null':
+            _value = None
+        else:
+            _value = value
+        setattr(self, key, _value)
+
+    def make_argpack(self) -> ArgPack:
         return ArgPack(self)
 
-    def save_argpack(self, argpack):
+    def save_argpack(self, argpack: ArgPack) -> None:
         self.service['argpack'].save(item=argpack)
 
-    def load_argpack(self, id):
+    def load_argpack(self, id: str) -> ArgPack:
         return self.service['argpack'].load(id=id)
 
-    def set_root(self, code):
-        if not isinstance(code, CodeBase):
+    def _set_root(self, code: Code) -> None:
+        if not isinstance(code, Code):
             raise TypeError(type(code))
         self.root = code
 
-    def __str__(self):
-        ret = '%s(id: %s, subscribe: %s)\n' % (self.__class__.__name__, self.id, self.subscribe)
+    def __str__(self) -> str:
+        ret = '%s(id: %s, subscribe: %s' % (self.__class__.__name__, self.id, self.subscribe)
+        additional_item = 'owner'
+        item = getattr(self, additional_item)
+        if item:
+            ret += ', %s: %s' % (additional_item, item)
+        ret += ')\n'
         stack = list()
         hierarchy = 0
         first_token = True
@@ -77,14 +106,14 @@ def __str__(self):
                     stack.append((c, h + 1))
         return ret
 
-    def __repr__(self):
+    def __repr__(self) -> str:
         return self.__str__()
 
-    def save_snapshot(self, timestamp=None, argpack=None):
+    def save_snapshot(self, timestamp: Optional[float] = None, argpack: Optional[Union[ArgPack, dict]] = None) -> None:
         snapshot = self.to_snapshot(argpack=argpack, timestamp=timestamp)
         self.service['snapshot'].save(snapshot)
 
-    def get_state(self):
+    def get_state(self) -> str:
         states = self.root.service['snapshot']\
             .load(serial_number=[code.serial_number for code in self.codes.values()], projection={'state'})
         if states:
@@ -102,10 +131,20 @@ def get_state(self):
                 return State(max_state).name
         return 'UNKNOWN'
 
-    def save(self, update=False):
+    def get_message(self) -> dict:
+        messages = self.root.service['snapshot'] \
+            .load(serial_number=[code.serial_number for code in self.codes.values()], projection={'id', 'message'})
+        ret = dict()
+        if messages:
+            for message in messages:
+                if message['message']:
+                    ret[message['id']] = message['message']
+        return ret
+
+    def save(self, update: bool = False) -> None:
         self.service['storage'].save(item=self, update=update)
 
-    def get_leaves(self):
+    def _get_leaves(self) -> set:
         leaves = set()
         q = Queue()
         q.put(self.root)
@@ -117,11 +156,11 @@ def get_leaves(self):
                 leaves.add(n)
         return leaves
 
-    def get_roots(self):
+    def _get_roots(self) -> set:
         roots = set()
         touched = set()
         q = Queue()
-        for leave in self.get_leaves():
+        for leave in self._get_leaves():
             q.put(leave)
             touched.add(leave.id)
         while not q.empty():
@@ -136,23 +175,23 @@ def get_roots(self):
                 roots.add(n)
         return roots
 
-    def recursive_run(self, code, argpack):
+    def _recursive_run(self, code: Code, argpack: Union[ArgPack, dict]) -> None:
         for p in code.parents.values():
             if p.get_state() != 'TERMINATED':
                 code.update_state('WAITING')
-                self.recursive_run(p, argpack)
+                self._recursive_run(p, argpack)
         code.update_state('READY')
         code(**argpack[code.id])
 
-    def sync_run(self, argpack):
-        for leave in self.get_leaves():
-            self.recursive_run(leave, argpack)
+    def sync_run(self, argpack: Union[ArgPack, dict]) -> None:
+        for leave in self._get_leaves():
+            self._recursive_run(leave, argpack)
 
-    def async_run(self, argpack=None):
+    def async_run(self, argpack: Union[ArgPack, dict]) -> None:
         for id, code in self.codes.items():
             code(**argpack[id])
 
-    def __call__(self, argpack=None, sync=True):
+    def __call__(self, argpack: Optional[Union[ArgPack, dict]] = None, sync: bool = True) -> Any:
         self.init_code_state(state='READY', argpack=argpack)
         self.save_snapshot(argpack=argpack)
         if not argpack:
@@ -163,7 +202,7 @@ def __call__(self, argpack=None, sync=True):
             self.async_run(argpack=argpack)
         return self.get_result()
 
-    def init_code_state(self, state, argpack=None):
+    def init_code_state(self, state: Union[State, str], argpack: Optional[Union[ArgPack, dict]] = None) -> None:
         for id, code in self.codes.items():
             if argpack and id in argpack:
                 if isinstance(argpack[id], dict):
@@ -172,23 +211,23 @@ def init_code_state(self, state, argpack=None):
                     _kwargs = argpack[id].to_dict()
                 code.update_state(state, kwargs=_kwargs)
 
-    def get_result(self):
+    def get_result(self) -> Optional[Any]:
         if self.subscribe and self.codes[self.subscribe].get_state() == 'TERMINATED':
             return self.codes[self.subscribe].get_result()
         else:
             return None
 
-    def to_dict(self):
+    def to_dict(self) -> dict:
         d = dict()
         d['_id'] = self.id
         d['subscribe'] = self.subscribe
         d['structure'] = self.get_structure()
         d['source'] = self.get_source()
+        d['owner'] = self.owner
         return d
 
     @classmethod
-    def from_dict(cls, d):
-        p = parser('Code(id: {id}, function: {function}, args: {args}, receive: {receive})')
+    def from_dict(cls, d: dict) -> 'CodePack':
         root = None
         stack = list()
         codes = dict()
@@ -196,9 +235,13 @@ def from_dict(cls, d):
         for i, line in enumerate(d['structure'].split('\n')):  # os.linesep
             split_idx = line.index('Code')
             hierarchy = len(line[1: split_idx-1])
-            attr = p.parse(line[split_idx:])
+            code_str = line[split_idx:]
+            p = parser(Code.blueprint(code_str))
+            attr = p.parse(code_str).named
             if attr['id'] not in codes:
-                codes[attr['id']] = Code(id=attr['id'], source=d['source'][attr['id']])
+                codes[attr['id']] = Code(id=attr['id'], source=d['source'][attr['id']],
+                                         env=attr.get('env', None), image=attr.get('image', None),
+                                         owner=attr.get('owner', None))
             code = codes[attr['id']]
             receive[code.id] = literal_eval(attr['receive'])
             if i == 0:
@@ -215,12 +258,12 @@ def from_dict(cls, d):
         for id, code in codes.items():
             for arg, sender in receive[id].items():
                 code.receive(arg) << codes[sender]
-        return cls(d['_id'], code=root, subscribe=d['subscribe'])
+        return cls(d['_id'], code=root, subscribe=d['subscribe'], owner=d.get('owner', None))
 
-    def get_source(self):
+    def get_source(self) -> dict:
         return {id: code.source for id, code in self.codes.items()}
 
-    def get_structure(self):
+    def get_structure(self) -> str:
         ret = str()
         stack = list()
         hierarchy = 0
@@ -238,11 +281,11 @@ def get_structure(self):
                     stack.append((c, h + 1))
         return ret
 
-    def to_snapshot(self, *args, **kwargs):
-        return CodePackSnapshot(self, *args, **kwargs)
+    def to_snapshot(self, *args: Any, **kwargs: Any) -> CodePackSnapshot:
+        return self.service['snapshot'].convert_to_snapshot(self, *args, **kwargs)
 
     @classmethod
-    def from_snapshot(cls, snapshot):
+    def from_snapshot(cls, snapshot: CodePackSnapshot) -> 'CodePack':
         d = snapshot.to_dict()
         d['_id'] = d['id']
         ret = cls.from_dict(d)
diff --git a/codepack/config/__init__.py b/codepack/config/__init__.py
index 7b2739d..e69de29 100644
--- a/codepack/config/__init__.py
+++ b/codepack/config/__init__.py
@@ -1,3 +0,0 @@
-from codepack.config.config import Config
-from codepack.config.default import Default
-from codepack.config.alias import Alias
diff --git a/codepack/config/alias.py b/codepack/config/alias.py
index 2685a2a..d023869 100644
--- a/codepack/config/alias.py
+++ b/codepack/config/alias.py
@@ -1,20 +1,17 @@
+from codepack.config.config import Config
 from importlib import import_module
-from codepack.config import Config
 import os
+import inspect
+from typing import Optional, Union
 
 
 class Alias:
     PREFIX = 'ALIAS'
 
-    def __init__(self, data=None):
+    def __init__(self, data: Optional[Union[str, dict]] = None) -> None:
         self.aliases = None
         if isinstance(data, str):
-            _aliases = Config.parse_config(section='alias', config_path=data)
-            if len(_aliases) == 1 and 'path' in _aliases:
-                alias_path = Config.get_value(section='alias', key='path', config=_aliases)
-                aliases = Config.parse_config(section='alias', config_path=alias_path)
-            else:
-                aliases = _aliases
+            aliases = Config.parse_config(section='alias', config_path=data)
             self.aliases = aliases
         elif isinstance(data, dict):
             self.aliases = data
@@ -24,14 +21,14 @@ def __init__(self, data=None):
             raise TypeError(type(data))  # pragma: no cover
 
     @classmethod
-    def get_env(cls, item):
+    def get_env(cls, item: str) -> str:
         return ('%s_%s_%s' % (Config.PREFIX, cls.PREFIX, item)).upper()
 
     @staticmethod
-    def get_class(module, name):
+    def get_class(module: str, name: str) -> type:
         return getattr(import_module(module), name)
 
-    def __getitem__(self, item):
+    def __getitem__(self, item: str) -> type:
         if self.aliases:
             path = self.aliases[item]
         elif self.get_env(item) in os.environ:
@@ -39,15 +36,27 @@ def __getitem__(self, item):
         elif '%s_ALIAS_PATH' % Config.PREFIX in os.environ:
             aliases = Config.parse_config(section='alias', config_path=os.environ['%s_ALIAS_PATH' % Config.PREFIX])
             path = aliases[item]
-        elif Config.LABEL_CONFIG_PATH in os.environ:
-            config = Config.parse_config(section='alias', config_path=os.environ.get(Config.LABEL_CONFIG_PATH), ignore_error=True)
-            alias_path = Config.get_value(section='alias', key='path', config=config)
-            aliases = Config.parse_config(section='alias', config_path=alias_path)
-            path = aliases[item]
         else:
-            raise AttributeError("%s not found in os.environ['%s'], os.environ['%s'], and os.environ['%s']"
-                                 % (item, self.get_env(item), '%s_ALIAS_PATH' % Config.PREFIX, Config.LABEL_CONFIG_PATH))
+            aliases = self.get_default_alias()
+            if aliases is not None:
+                path = aliases[item]
+            else:
+                raise AttributeError("%s not found in os.environ['%s'], os.environ['%s'], and os.environ['%s']"
+                                     % (item, self.get_env(item),
+                                        '%s_ALIAS_PATH' % Config.PREFIX, Config.LABEL_CONFIG_PATH))
         tokens = path.split('.')
         module = '.'.join(tokens[:-1])
         name = tokens[-1]
         return self.get_class(module, name)
+
+    @classmethod
+    def get_default_alias(cls) -> Optional[dict]:
+        default_config_dir = os.path.dirname(os.path.abspath(inspect.getfile(cls)))
+        default_alias_path = os.path.join(default_config_dir, 'default', 'alias.ini')
+        if os.path.isfile(default_alias_path):
+            try:
+                return Config.parse_config(section='alias', config_path=default_alias_path)
+            except Exception:
+                return None
+        else:
+            return None
diff --git a/codepack/config/config.py b/codepack/config/config.py
index 4c8966b..067cbf5 100644
--- a/codepack/config/config.py
+++ b/codepack/config/config.py
@@ -1,16 +1,26 @@
 from configparser import ConfigParser
 import os
+import logging
+from logging.config import dictConfig
+import json
+import inspect
+from typing import Optional
 
 
 class Config:
     PREFIX = 'CODEPACK'
+    LABEL_CONFIG_DIR = '%s_CONFIG_DIR' % PREFIX
     LABEL_CONFIG_PATH = '%s_CONFIG_PATH' % PREFIX
+    LABEL_LOGGER_LOG_DIR = '%s_LOGGER_LOG_DIR' % PREFIX
 
-    def __init__(self, config_path: str = None):
-        self.config_path = config_path
+    def __init__(self, config_path: Optional[str] = None) -> None:
+        if config_path:
+            self.config_path = self.get_config_path(path=config_path)
+        else:
+            self.config_path = None
 
     @staticmethod
-    def parse_config(section: str, config_path: str, ignore_error: bool = False):
+    def parse_config(section: str, config_path: str, ignore_error: bool = False) -> Optional[dict]:
         cp = ConfigParser()
         cp.read(config_path)
         if ignore_error and not cp.has_section(section):
@@ -18,54 +28,161 @@ def parse_config(section: str, config_path: str, ignore_error: bool = False):
         items = cp.items(section)
         return {item[0]: item[1] for item in items}
 
-    def get_config(self, section: str, config_path: str = None, ignore_error: bool = False):
-        _config_path = config_path
-        if not _config_path:
+    def get_config(self, section: str, config_path: Optional[str] = None,
+                   ignore_error: Optional[bool] = False) -> Optional[dict]:
+        overwrite_with_os_env = False
+        parse_default_config = False
+        _config_path = None
+        if config_path:
+            _config_path = self.get_config_path(path=config_path)
+        elif self.config_path:
             _config_path = self.config_path
-        if not _config_path:
-            _config_path = os.environ.get(self.LABEL_CONFIG_PATH, None)
+        elif self.LABEL_CONFIG_PATH in os.environ:
+            overwrite_with_os_env = True
+            _config_path = self.get_config_path(os.environ.get(self.LABEL_CONFIG_PATH))
+        else:
+            overwrite_with_os_env = True
+            parse_default_config = True
         if _config_path:
-            return self.parse_config(section=section, config_path=_config_path)
+            ret = self.parse_config(section=section, config_path=self.get_config_path(_config_path))
+        elif parse_default_config:
+            ret = self.get_default_config(section=section)
         elif ignore_error:
             return None
         else:
-            raise AttributeError("path of configuration file should be provided in either 'config_path' or os.environ['%s']"
-                                 % self.LABEL_CONFIG_PATH)
+            raise AttributeError(
+                "path of configuration file should be provided in either 'config_path' or os.environ['%s']"
+                % self.LABEL_CONFIG_PATH)
+        if ret and overwrite_with_os_env:
+            ret = self.collect_values(section=section, config=ret, ignore_error=ignore_error)
+        return ret
+
+    @classmethod
+    def _os_env_missing_error_message(cls, section: str, key: str) -> str:
+        return "'%s' information should be provided in os.environ['%s']" % (section, cls.os_env(key=section, value=key))
+
+    @classmethod
+    def _assert_mandatory_keys(cls, section: str, mandatory_keys: list, config: dict) -> None:
+        for key in mandatory_keys:
+            assert key in config, cls._os_env_missing_error_message(section=section, key=key)
 
     @classmethod
-    def get_value(cls, section: str, key: str, config: dict = None):
-        env = '%s_%s_%s' % (cls.PREFIX, section.upper(), key.upper())
-        if config:
-            ret = config[key]
-        elif env in os.environ:
-            ret = os.environ.get(env, None)
+    def collect_value(cls, section: str, key: str, config: dict, ignore_error: bool = False) -> Optional[str]:
+        env = cls.os_env(key=section, value=key)
+        if env in os.environ:
+            value = os.environ.get(env)
+        elif key in config:
+            value = config[key]
         else:
-            raise AssertionError("'%s' information should be provided in os.environ['%s']" % (section, env))
-        return ret
+            default_config = cls.get_default_config(section=section)
+            if default_config and key in default_config:
+                value = default_config[key]
+            elif ignore_error:
+                return None
+            else:
+                raise AssertionError(cls._os_env_missing_error_message(section=section, key=key))
+        if section in {'conn', 'alias', 'logger'} and key in {'path', 'config_path'}:
+            value = cls.get_config_path(value)
+        return value
+
+    @classmethod
+    def collect_values(cls, section: str, config: dict, ignore_error: bool = False) -> dict:
+        values = dict()
+        for key in config.keys():
+            values[key] = cls.collect_value(section=section, key=key, config=config, ignore_error=ignore_error)
+        for key, value in {k: v for k, v in os.environ.items() if cls.os_env(key=section) in k}.items():
+            k = key.replace(cls.os_env(key=section), '').lower()
+            if k not in values:
+                values[k] = cls.collect_value(section=section, key=k, config=dict())
+        default_config = cls.get_default_config(section=section)
+        for key, value in default_config.items():
+            if key not in values:
+                values[key] = value
+        return values
 
-    def get_storage_config(self, section: str, config_path: str = None):
-        config = self.get_config(section=section, config_path=config_path, ignore_error=True)
-        ret = dict()
-        ret['source'] = self.get_value(section=section, key='source', config=config)
-        if ret['source'] == 'memory':
+    def get_storage_config(self, section: str, config_path: Optional[str] = None) -> dict:
+        config = self.get_config(section=section, config_path=config_path)
+        source = config.get('source', None)
+        if source == 'memory':
             pass
-        elif ret['source'] == 'file':
-            ret['path'] = self.get_value(section=section, key='path', config=config)
-        elif ret['source'] == 'mongodb':
-            ret['db'] = self.get_value(section=section, key='db', config=config)
-            ret['collection'] = self.get_value(section=section, key='collection', config=config)
-            conn_config = self.get_config(section='conn', config_path=config_path, ignore_error=True)
-            conn_config_path = self.get_value(section='conn', key='path', config=conn_config)
-            ret['mongodb'] = self.parse_config(section='mongodb', config_path=conn_config_path)
-        elif ret['source'] == 'kafka':
-            ret['topic'] = self.get_value(section=section, key='topic', config=config)
-            conn_config = self.get_config(section='conn', config_path=config_path, ignore_error=True)
-            conn_config_path = self.get_value(section='conn', key='path', config=conn_config)
-            ret['kafka'] = self.parse_config(section='kafka', config_path=conn_config_path)
+        elif source == 'file':
+            self._assert_mandatory_keys(section=section, mandatory_keys=['path'], config=config)
+        elif source == 'mongodb':
+            self._assert_mandatory_keys(section=section, mandatory_keys=['db', 'collection'], config=config)
+            config[source] = self.get_config(section=source, config_path=config_path)
+        elif source == 'kafka':
+            self._assert_mandatory_keys(section=section, mandatory_keys=['topic'], config=config)
+            config[source] = self.get_config(section=source, config_path=config_path)
+        elif source in {'docker', 's3'}:
+            config[source] = self.get_config(section=source, config_path=config_path)
         else:
-            raise NotImplementedError("'%s' is not implemented" % ret['source'])
-        if config:
-            for k in config:
-                if k not in ret:
-                    ret[k] = self.get_value(section=section, key=k, config=config)
+            raise NotImplementedError("'%s' source is not implemented" % source)
+        return config
+
+    @staticmethod
+    def get_logger(config_path: str, name: Optional[str] = None) -> logging.Logger:
+        with open(config_path, 'r') as f:
+            config = json.load(f)
+            if 'handlers' in config:
+                for handler in config['handlers'].values():
+                    for k, v in handler.items():
+                        if k == 'filename':
+                            log_dir = Config.get_log_dir()
+                            log_file = os.path.join(log_dir, v)
+                            if not os.path.exists(log_dir):
+                                os.makedirs(log_dir)
+                            handler.update(filename=log_file)
+            dictConfig(config)
+        return logging.getLogger(name=name)
+
+    @staticmethod
+    def get_log_dir() -> str:
+        return os.environ.get(Config.LABEL_LOGGER_LOG_DIR, 'logs')
+
+    @classmethod
+    def get_default_config_dir(cls) -> str:
+        return os.path.join(os.path.dirname(os.path.abspath(inspect.getfile(cls))), 'default')
+
+    @classmethod
+    def get_default_config_path(cls) -> str:
+        return os.path.join(cls.get_default_config_dir(), 'default.ini')
+
+    @classmethod
+    def get_default_config(cls, section: str) -> Optional[dict]:
+        default_config_path = cls.get_default_config_path()
+        ret = None
+        try:
+            if os.path.isfile(default_config_path):
+                ret = cls.parse_config(section=section, config_path=default_config_path)
+                default_dir = cls.get_default_config_dir()
+                if section == 'callback' and 'source' in ret and ret['source'] == 'file' and 'path' not in ret:
+                    ret['path'] = os.path.join(default_dir, 'scripts')
+                elif section == 'docker_manager' and 'path' not in ret:
+                    ret['path'] = os.path.join(default_dir, 'scripts')
+                elif section == 'logger' and 'config_path' not in ret:
+                    ret['config_path'] = os.path.join(default_dir, 'logging.json')
+                elif section == 'worker' and 'script_path' not in ret:
+                    ret['script_path'] = os.path.join(default_dir, 'scripts/run_snapshot.py')
+        except Exception:
+            return None
+        finally:
+            return ret
+
+    @classmethod
+    def os_env(cls, key: str, value: Optional[str] = None) -> str:
+        ret = '%s_%s_' % (cls.PREFIX, key.replace('_', '').upper())
+        if value:
+            ret += value.upper()
+        return ret
+
+    @classmethod
+    def get_config_path(cls, path: str) -> str:
+        ret = path
+        if not os.path.exists(path):
+            if cls.LABEL_CONFIG_DIR in os.environ:
+                ret = os.path.join(os.environ.get(cls.LABEL_CONFIG_DIR), ret)
+            else:
+                raise AssertionError("config directory should be provided in os.environ['%s']" % cls.LABEL_CONFIG_DIR)
+        if not os.path.exists(ret):
+            raise FileNotFoundError("'%s' does not exist" % ret)
         return ret
diff --git a/codepack/config/default.py b/codepack/config/default.py
index a4a3ce4..2c711c4 100644
--- a/codepack/config/default.py
+++ b/codepack/config/default.py
@@ -1,7 +1,15 @@
 from codepack.config.config import Config
 from codepack.config.alias import Alias
-import os
-from inspect import getfullargspec
+import inspect
+from docker.errors import DockerException
+from typing import Optional, TypeVar
+
+
+Service = TypeVar('Service', bound='codepack.plugin.service.Service')
+Scheduler = TypeVar('Scheduler', bound='codepack.plugin.scheduler.Scheduler')
+Employee = TypeVar('Employee', bound='codepack.plugin.employee.Employee')
+Manager = TypeVar('Manager', bound='codepack.plugin.manager.Manager')
+Logger = TypeVar('Logger', bound='logging.Logger')
 
 
 class Default:
@@ -10,61 +18,59 @@ class Default:
     instances = dict()
 
     @classmethod
-    def __init__(cls, config_path: str = None, alias_path: str = None):
+    def __init__(cls, config_path: Optional[str] = None, alias_path: Optional[str] = None) -> None:
         cls.init(config_path=config_path, alias_path=alias_path)
 
     @classmethod
-    def init(cls, config_path: str = None, alias_path: str = None):
+    def init(cls, config_path: Optional[str] = None, alias_path: Optional[str] = None) -> None:
         cls.init_config(config_path=config_path)
-        cls.init_alias(config_path=config_path, alias_path=alias_path)
+        cls.init_alias(alias_path=alias_path)
         cls.init_instances()
 
     @classmethod
-    def init_config(cls, config_path: str = None):
-        cls.config = cls._get_config(config_path=config_path)
-
-    @staticmethod
-    def _get_config(config_path: str = None):
-        return Config(config_path=config_path)
+    def init_config(cls, config_path: Optional[str] = None) -> None:
+        cls.config = Config(config_path=config_path)
 
     @classmethod
-    def _get_storage_config(cls, section: str, config_path: str = None):
+    def get_config_instance(cls, config_path: Optional[str] = None) -> Config:
         if config_path:
-            _config = cls._get_config(config_path=config_path)
+            _config = Config(config_path=config_path)
         elif not cls.config:
             cls.init_config()
             _config = cls.config
         else:
             _config = cls.config
-        return _config.get_storage_config(section=section)
+        return _config
 
     @classmethod
-    def init_alias(cls, config_path: str = None, alias_path: str = None):
-        cls.alias = cls._get_alias(config_path=config_path, alias_path=alias_path)
+    def init_alias(cls, alias_path: Optional[str] = None) -> None:
+        cls.alias = Alias(data=alias_path)
 
-    @staticmethod
-    def _get_alias(config_path: str = None, alias_path: str = None):
+    @classmethod
+    def get_alias_instance(cls, alias_path: Optional[str] = None) -> Alias:
         if alias_path:
-            return Alias(data=alias_path)
-        elif config_path:
-            return Alias(data=config_path)
+            _alias = Alias(data=alias_path)
+        elif not cls.alias:
+            cls.init_alias()
+            _alias = cls.alias
         else:
-            return Alias()
+            _alias = cls.alias
+        return _alias
 
     @classmethod
-    def init_instances(cls):
+    def init_instances(cls) -> None:
         cls.instances = dict()
 
     @classmethod
-    def set_config(cls, config: Config):
-        cls.config = config
+    def set_config(cls, config: Config) -> None:
+        cls.config = config  # pragma: no cover
 
     @classmethod
-    def set_alias(cls, alias: Alias):
-        cls.alias = alias
+    def set_alias(cls, alias: Alias) -> None:
+        cls.alias = alias  # pragma: no cover
 
     @staticmethod
-    def get_alias_from_source(source: str, prefix: str = None, suffix: str = None):
+    def get_alias_from_source(source: str, prefix: Optional[str] = None, suffix: Optional[str] = None) -> str:
         if source == 'mongodb':
             ret = 'mongo'
         else:
@@ -76,43 +82,49 @@ def get_alias_from_source(source: str, prefix: str = None, suffix: str = None):
         return ret
 
     @classmethod
-    def get_class_from_alias(cls, alias: str, config_path: str = None, alias_path: str = None):
-        if config_path or alias_path:
-            _alias = cls._get_alias(config_path=config_path, alias_path=alias_path)
-        elif not cls.alias:
-            cls.init_alias()
-            _alias = cls.alias
-        else:
-            _alias = cls.alias
-        return _alias[alias]
+    def get_class_from_alias(cls, alias: str, alias_path: Optional[str] = None) -> type:
+        return cls.get_alias_instance(alias_path=alias_path)[alias]
 
     @classmethod
-    def get_storage_instance(cls, section: str, instance_type: str, config_path: str = None, alias_path: str = None):
-        key = '%s_%s' % (section, instance_type)
+    def get_service(cls, section: str, service_type: str,
+                    config_path: Optional[str] = None, alias_path: Optional[str] = None) -> Service:
+        key = '%s_%s' % (section, service_type)
         if config_path or alias_path or key not in cls.instances:
-            storage_config = cls._get_storage_config(section=section, config_path=config_path)
+            config = cls.get_config_instance(config_path=config_path)
+            storage_config = config.get_storage_config(section=section, config_path=config_path)
             source = storage_config.pop('source')
-            alias = cls.get_alias_from_source(source=source, suffix=instance_type)
-            c = cls.get_class_from_alias(alias, config_path=config_path, alias_path=alias_path)
-            item_type = cls.get_class_from_alias(section, config_path=config_path, alias_path=alias_path)
-            _instance = c(item_type=item_type, **storage_config)
+            storage_alias = cls.get_alias_from_source(source=source, suffix='storage')
+            storage_class = cls.get_class_from_alias(storage_alias, alias_path=alias_path)
+            service_class = cls.get_class_from_alias(service_type, alias_path=alias_path)
+            item_type = cls.get_class_from_alias(section, alias_path=alias_path)
+            storage_instance = storage_class(item_type=item_type, **storage_config)
+            service_instance = service_class(storage=storage_instance)
             if config_path is None and alias_path is None:
-                cls.instances[key] = _instance
-            return _instance
+                cls.instances[key] = service_instance
+            return service_instance
         else:
             return cls.instances[key]
 
     @classmethod
-    def get_scheduler(cls, section: str = 'scheduler', config_path: str = None, alias_path: str = None):
+    def get_scheduler(cls, section: str = 'scheduler',
+                      config_path: Optional[str] = None, alias_path: Optional[str] = None) -> Scheduler:
         key = section
         if config_path or alias_path or key not in cls.instances:
-            storage_config = cls._get_storage_config(section=section, config_path=config_path)
+            config = cls.get_config_instance(config_path=config_path)
+            storage_config = config.get_storage_config(section=section, config_path=config_path)
             source = storage_config.pop('source')
-            alias = cls.get_alias_from_source(source=source, suffix=section)
-            c = cls.get_class_from_alias(alias, config_path=config_path, alias_path=alias_path)
-            if 'CODEPACK_SCHEDULER_SUPERVISOR' in os.environ:
-                storage_config['supervisor'] = os.environ['CODEPACK_SCHEDULER_SUPERVISOR']
-            _instance = c(**storage_config)
+            jobstore_alias = cls.get_alias_from_source(source=source, suffix='jobstore')
+            jobstore_class = cls.get_class_from_alias(jobstore_alias, alias_path=alias_path)
+            scheduler_class = cls.get_class_from_alias(section, alias_path=alias_path)
+            scheduler_config = dict()
+            remaining_config = dict()
+            for k, v in storage_config.items():
+                if k in inspect.getfullargspec(scheduler_class.__init__).args:
+                    scheduler_config[k] = v
+                else:
+                    remaining_config[k] = v
+            jobstore_instance = jobstore_class(**remaining_config)
+            _instance = scheduler_class(jobstore=jobstore_instance, **scheduler_config)
             if config_path is None and alias_path is None:
                 cls.instances[key] = _instance
             return _instance
@@ -120,29 +132,89 @@ def get_scheduler(cls, section: str = 'scheduler', config_path: str = None, alia
             return cls.instances[key]
 
     @classmethod
-    def get_employee(cls, section: str, config_path: str = None, alias_path: str = None):
+    def get_employee(cls, section: str, config_path: Optional[str] = None,
+                     alias_path: Optional[str] = None) -> Employee:
         key = section
         if 'worker' in section:
-            kafka_type = 'consumer'
+            messenger_type = 'consumer'
         elif 'supervisor' in section:
-            kafka_type = 'producer'
+            messenger_type = 'producer'
         else:
             raise NotImplementedError("'%s' is unknown")
         if config_path or alias_path or key not in cls.instances:
-            storage_config = cls._get_storage_config(section=section, config_path=config_path)
-            storage_config.pop('source')
-            kafka_config = storage_config.pop('kafka')
-            c = cls.get_class_from_alias(section, config_path=config_path, alias_path=alias_path)
+            config = cls.get_config_instance(config_path=config_path)
+            storage_config = config.get_storage_config(section=section, config_path=config_path)
+            source = storage_config.pop('source')
+            storage_alias = cls.get_alias_from_source(source=source, suffix='messenger')
+            storage_class = cls.get_class_from_alias(storage_alias, alias_path=alias_path)
+            if source == 'memory':
+                conn_config = dict()
+            else:
+                conn_config = storage_config.pop(source)
+            employee_class = cls.get_class_from_alias(section, alias_path=alias_path)
             employee_config = dict()
             for k, v in storage_config.items():
-                if k in getfullargspec(c.__init__).args:
+                if k in inspect.getfullargspec(employee_class.__init__).args:
                     employee_config[k] = v
                 else:
-                    kafka_config[k] = v
-            employee_config[kafka_type] = kafka_config
-            _instance = c(**employee_config)
+                    conn_config[k] = v
+            if source == 'kafka':
+                storage_instance = storage_class(**{messenger_type: conn_config})
+            else:
+                storage_instance = storage_class(**conn_config)
+            _instance = employee_class(messenger=storage_instance, **employee_config)
             if config_path is None and alias_path is None:
                 cls.instances[key] = _instance
             return _instance
         else:
             return cls.instances[key]
+
+    @classmethod
+    def get_docker_manager(cls, section: str = 'docker_manager',
+                           config_path: Optional[str] = None, alias_path: Optional[str] = None) -> Manager:
+        key = section
+        if config_path or alias_path or key not in cls.instances:
+            config = cls.get_config_instance(config_path=config_path)
+            storage_config = config.get_storage_config(section=section, config_path=config_path)
+            storage_config.pop('source')
+            manager_class = cls.get_class_from_alias(section, alias_path=alias_path)
+            try:
+                _instance = manager_class(**storage_config)
+            except DockerException:
+                return None
+            if config_path is None and alias_path is None:
+                cls.instances[key] = _instance
+            return _instance
+        else:
+            return cls.instances[key]
+
+    @classmethod
+    def get_interpreter_manager(cls, section: str = 'interpreter_manager',
+                                config_path: Optional[str] = None, alias_path: Optional[str] = None) -> Manager:
+        key = section
+        if config_path or alias_path or key not in cls.instances:
+            config = cls.get_config_instance(config_path=config_path)
+            manager_config = config.get_config(section=section, config_path=config_path, ignore_error=True)
+            manager_class = cls.get_class_from_alias(section, alias_path=alias_path)
+            instance = manager_class(**manager_config)
+            if config_path is None and alias_path is None:
+                cls.instances[key] = instance
+            return instance
+        else:
+            return cls.instances[key]
+
+    @classmethod
+    def get_logger(cls, name: Optional[str] = None, config_path: Optional[str] = None) -> Logger:
+        key = 'logger.%s' % (name if name else 'root')
+        if config_path or key not in cls.instances:
+            config = cls.get_config_instance(config_path=config_path)
+            logger_config = config.get_config('logger', config_path=config_path)
+            logger_name = logger_config.get('name', None)
+            if name is not None:
+                logger_name = name
+            logger = config.get_logger(config_path=logger_config['config_path'], name=logger_name)
+            if config_path is None:
+                cls.instances[key] = logger
+            return logger
+        else:
+            return cls.instances[key]
diff --git a/codepack/config/default/alias.ini b/codepack/config/default/alias.ini
new file mode 100644
index 0000000..e457e3b
--- /dev/null
+++ b/codepack/config/default/alias.ini
@@ -0,0 +1,27 @@
+[alias]
+code = codepack.code.Code
+codepack = codepack.codepack.CodePack
+arg = codepack.arg.Arg
+argpack = codepack.argpack.ArgPack
+code_snapshot = codepack.snapshot.code_snapshot.CodeSnapshot
+codepack_snapshot = codepack.snapshot.codepack_snapshot.CodePackSnapshot
+delivery = codepack.delivery.delivery.Delivery
+callback = codepack.callback.callback.Callback
+storage_service = codepack.plugin.storage_service.StorageService
+delivery_service = codepack.plugin.delivery_service.DeliveryService
+snapshot_service = codepack.plugin.snapshot_service.SnapshotService
+callback_service = codepack.plugin.callback_service.CallbackService
+mongo_scheduler = codepack.plugin.mongo_scheduler.MongoScheduler
+supervisor = codepack.plugin.supervisor.Supervisor
+scheduler = codepack.plugin.scheduler.Scheduler
+worker = codepack.plugin.worker.Worker
+docker_manager = codepack.plugin.docker_manager.DockerManager
+interpreter_manager = codepack.plugin.interpreter_manager.InterpreterManager
+memory_storage = codepack.storage.memory_storage.MemoryStorage
+file_storage = codepack.storage.file_storage.FileStorage
+mongo_storage = codepack.storage.mongo_storage.MongoStorage
+memory_messenger = codepack.storage.memory_messenger.MemoryMessenger
+kafka_messenger = codepack.storage.kafka_messenger.KafkaMessenger
+s3_storage = codepack.storage.s3_storage.S3Storage
+memory_jobstore = codepack.storage.memory_jobstore.MemoryJobStore
+mongo_jobstore = codepack.storage.mongo_jobstore.MongoJobStore
diff --git a/codepack/config/default/default.ini b/codepack/config/default/default.ini
new file mode 100644
index 0000000..d95660f
--- /dev/null
+++ b/codepack/config/default/default.ini
@@ -0,0 +1,111 @@
+[code]
+source = memory
+
+[codepack]
+source = memory
+
+[argpack]
+source = memory
+
+[delivery]
+source = memory
+
+[code_snapshot]
+source = memory
+
+[codepack_snapshot]
+source = memory
+
+[callback]
+source = file
+; path = scripts
+
+[apiserver]
+scheduler = self
+
+[scheduler]
+source = memory
+
+[worker]
+source = memory
+topic = codepack
+interval = 1
+; script_path = scripts/run_snapshot.py
+logger = worker-logger
+background = True
+
+[supervisor]
+source = memory
+topic = codepack
+
+[docker_manager]
+source = docker
+; path = scripts
+run_opt = {"dns": ["8.8.8.8"]}
+
+[interpreter_manager]
+path = ${HOME}/anaconda3/envs
+run_opt = {}
+
+[logger]
+; config_path = logging.json
+name = default-logger
+log_dir = logs
+
+[ssh]
+ssh_host = 1.2.3.4
+ssh_port = 22
+ssh_username = ?
+ssh_password = ?
+
+[mongodb]
+host = localhost
+port = 27017
+
+[mysql]
+host = localhost
+port = 3306
+user = ?
+passwd = ?
+charset = utf8
+cursorclass = pymysql.cursors.Cursor
+; cursorclass = pymysql.cursors.DictCursor
+
+[mssql]
+host = localhost
+port = 1433
+user = ?
+password = ?
+charset = utf8
+as_dict = False
+
+[oracledb]
+host = localhost
+port = 1621
+service_name = ?
+user = ?
+password = ?
+as_dict = False
+
+[kafka]
+bootstrap_servers=?:9092,?:9092,?:9092
+
+[rest_api]
+my_service = https://?:?/?
+
+[dynamodb]
+service_name = dynamodb
+region_name = ?
+endpoint_url = https://?
+aws_access_key_id = ?
+aws_secret_access_key = ?
+
+[docker]
+base_url = unix://var/run/docker.sock
+
+[s3]
+service_name = s3
+region_name = ?
+endpoint_url = https://?
+aws_access_key_id = ?
+aws_secret_access_key = ?
diff --git a/codepack/config/default/logging.json b/codepack/config/default/logging.json
new file mode 100644
index 0000000..72304d1
--- /dev/null
+++ b/codepack/config/default/logging.json
@@ -0,0 +1,46 @@
+{
+    "version": 1,
+    "formatters": {
+        "default": {
+            "format": "%(asctime)s %(levelname)-8s %(module)s.%(funcName)s[%(lineno)s]: %(message)s"
+        }
+    },
+    "handlers": {
+        "console": {
+            "class": "logging.StreamHandler",
+            "level": "INFO",
+            "formatter": "default"
+        },
+        "error-file": {
+            "class": "logging.handlers.TimedRotatingFileHandler",
+            "level": "ERROR",
+            "formatter": "default",
+            "filename": "error.log",
+            "when": "midnight"
+        },
+        "worker": {
+            "class": "logging.handlers.TimedRotatingFileHandler",
+            "level": "INFO",
+            "formatter": "default",
+            "filename": "worker.log",
+            "when": "midnight"
+        }
+    },
+    "loggers": {
+        "default-logger": {
+            "level": "INFO",
+            "handlers": ["console"],
+            "propagate": true
+        },
+        "error-logger": {
+            "level": "ERROR",
+            "handlers": ["console", "error-file"],
+            "propagate": true
+        },
+        "worker-logger": {
+            "level": "INFO",
+            "handlers": ["console", "worker"],
+            "propagate": true
+        }
+    }
+}
diff --git a/codepack/config/default/scripts/run_snapshot.py b/codepack/config/default/scripts/run_snapshot.py
new file mode 100644
index 0000000..9c33f5b
--- /dev/null
+++ b/codepack/config/default/scripts/run_snapshot.py
@@ -0,0 +1,43 @@
+from codepack import Code
+from codepack import CodeSnapshot
+from codepack import CallbackService
+from codepack.storage import FileStorage
+from codepack import Callback
+from codepack import Default
+from codepack import Worker
+import argparse
+import sys
+from functools import partial
+
+
+if __name__ == "__main__":
+    parser = argparse.ArgumentParser()
+    parser.add_argument('snapshot', metavar='SNAPSHOT', type=str, help='a JSON file of the snapshot to run')
+    parser.add_argument('-c', '--callback', action='append', help='callback functions for the snapshot')
+    parser.add_argument('-p', '--path', help='path to the directory including callback functions')
+    parser.add_argument('-l', '--logger', help='a logger name to write logs')
+    args = parser.parse_args()
+    if args.logger:
+        logger = Default.get_logger(args.logger)
+        sys.stdout.write = partial(Worker.log, logger.info)
+    else:
+        logger = None
+    try:
+        callback_storage = FileStorage(item_type=Callback, path=args.path if args.path else '.')
+        callback_service = CallbackService(storage=callback_storage)
+        code_snapshot = CodeSnapshot.from_file(args.snapshot)
+        code_args = code_snapshot.args
+        code_kwargs = code_snapshot.kwargs
+        code = Code.from_snapshot(code_snapshot)
+        if args.callback:
+            callbacks = list()
+            for name in args.callback:
+                cb = callback_service.pull(name)
+                callbacks.append(cb)
+            code.register_callback(callbacks)
+        code(*code_args, **code_kwargs)
+    except Exception as e:
+        if logger:
+            logger.error(e)
+        else:
+            print(e)
diff --git a/codepack/delivery/__init__.py b/codepack/delivery/__init__.py
index 7fb78d0..e69de29 100644
--- a/codepack/delivery/__init__.py
+++ b/codepack/delivery/__init__.py
@@ -1,1 +0,0 @@
-from codepack.delivery.delivery import Delivery
diff --git a/codepack/delivery/delivery.py b/codepack/delivery/delivery.py
index 7d2e099..1d0ef14 100644
--- a/codepack/delivery/delivery.py
+++ b/codepack/delivery/delivery.py
@@ -1,31 +1,34 @@
-from codepack.storage import Storable
+from codepack.storage.storable import Storable
 import json
 from datetime import datetime
+from typing import Any, Optional
 
 
 class Delivery(Storable):
-    def __init__(self, id, serial_number, item=None, timestamp=None):
+    def __init__(self, id: str, serial_number: str,
+                 item: Optional[Any] = None, timestamp: Optional[float] = None) -> None:
         Storable.__init__(self, id=id, serial_number=serial_number)
         self.item = item
         self.timestamp = timestamp if timestamp else datetime.now().timestamp()
 
-    def __str__(self):
+    def __str__(self) -> str:
         return '%s(id: %s, serial_number: %s)' % (self.__class__.__name__, self.id, self.serial_number)  # pragma: no cover
 
-    def __repr__(self):
+    def __repr__(self) -> str:
         return self.__str__()  # pragma: no cover
 
-    def send(self, item, timestamp=None):
+    def send(self, item: Any, timestamp: Optional[float] = None) -> None:
         timestamp if timestamp else datetime.now().timestamp()
         self.item = item
 
-    def receive(self):
+    def receive(self) -> Any:
         return self.item
 
-    def to_dict(self):
-        return {'_id': self.serial_number, 'id': self.id,
+    def to_dict(self) -> dict:
+        return {'_id': self.serial_number, 'id': self.id, 'serial_number': self.serial_number,
                 'timestamp': self.timestamp, 'item': json.dumps(self.item)}
 
     @classmethod
-    def from_dict(cls, d):
-        return cls(id=d['id'], serial_number=d['_id'], item=json.loads(d['item']), timestamp=d.get('timestamp', None))
+    def from_dict(cls, d: dict) -> 'Delivery':
+        return cls(id=d['id'], serial_number=d['serial_number'],
+                   item=json.loads(d['item']), timestamp=d.get('timestamp', None))
diff --git a/codepack/dependency/__init__.py b/codepack/dependency/__init__.py
index 1a760b8..e69de29 100644
--- a/codepack/dependency/__init__.py
+++ b/codepack/dependency/__init__.py
@@ -1,3 +0,0 @@
-from codepack.dependency.dependency import Dependency
-from codepack.dependency.dependency_manager import DependencyManager
-from codepack.dependency.dependency_state import DependencyState
diff --git a/codepack/dependency/dependency.py b/codepack/dependency/dependency.py
index 912ea6b..97d21d2 100644
--- a/codepack/dependency/dependency.py
+++ b/codepack/dependency/dependency.py
@@ -1,15 +1,20 @@
-from codepack.storage import Storable
+from codepack.storage.storable import Storable
+from typing import TypeVar, Optional, Union
+
+
+Code = TypeVar('Code', bound='codepack.code.Code')
 
 
 class Dependency(Storable):
-    def __init__(self, code=None, id=None, serial_number=None, arg=None):
+    def __init__(self, code: Optional[Code] = None, id: Optional[str] = None, serial_number: Optional[str] = None,
+                 arg: Optional[str] = None) -> None:
         super().__init__(id=id, serial_number=serial_number)
         self.code = None
         self.arg = None
         self.bind(code)
         self.depend_on(id=id, serial_number=serial_number, arg=arg)
 
-    def __lshift__(self, sender):
+    def __lshift__(self, sender: Union[Storable, dict]) -> None:
         if isinstance(sender, Storable):
             self.depend_on(id=sender.id, serial_number=sender.serial_number, arg=self.arg)
         elif isinstance(sender, dict):
@@ -18,15 +23,16 @@ def __lshift__(self, sender):
             raise TypeError(type(sender))
         self.code.add_dependency(self)
 
-    def bind(self, code):
+    def bind(self, code: Code) -> None:
         self.code = code
 
-    def depend_on(self, id=None, serial_number=None, arg=None):
+    def depend_on(self, id: Optional[str] = None, serial_number: Optional[str] = None,
+                  arg: Optional[str] = None) -> None:
         self.id = id
         self.serial_number = serial_number
         self.arg = arg
 
-    def __eq__(self, other):
+    def __eq__(self, other: Union['Dependency', dict]) -> bool:
         if isinstance(other, type(self)):
             ret = True
             ret &= (self.serial_number == other.serial_number)
@@ -42,15 +48,15 @@ def __eq__(self, other):
         else:
             return False
 
-    def __str__(self):
+    def __str__(self) -> str:
         return '%s(arg: %s, id: %s)' % (self.__class__.__name__, self.arg, self.id)  # pragma: no cover
 
-    def __repr__(self):
+    def __repr__(self) -> str:
         return self.__str__()  # pragma: no cover
 
-    def to_dict(self):
+    def to_dict(self) -> dict:
         return {'id': self.id, 'serial_number': self.serial_number, 'arg': self.arg}
 
     @classmethod
-    def from_dict(cls, d):
+    def from_dict(cls, d: dict) -> 'Dependency':
         return cls(id=d['id'], serial_number=d['serial_number'], arg=d['arg'])
diff --git a/codepack/dependency/dependency_manager.py b/codepack/dependency/dependency_manager.py
deleted file mode 100644
index dd36572..0000000
--- a/codepack/dependency/dependency_manager.py
+++ /dev/null
@@ -1,90 +0,0 @@
-from codepack.storage import MemoryStorage
-from codepack.dependency.dependency import Dependency
-from codepack.dependency.dependency_state import DependencyState
-from collections.abc import Iterable
-
-
-class DependencyManager(MemoryStorage, Iterable):
-    def __init__(self, code, item_type=Dependency):
-        MemoryStorage.__init__(self, item_type=item_type)
-        self.code = code
-
-    def add(self, dependency):
-        if isinstance(dependency, Dependency):
-            self.code.assert_arg(dependency.arg)
-            self.memory[dependency.serial_number] = dependency
-        elif isinstance(dependency, dict):
-            self.code.assert_arg(dependency['arg'])
-            self.memory[dependency['serial_number']] = Dependency.from_dict(d=dependency)
-            self.memory[dependency['serial_number']].bind(self.code)
-        elif isinstance(dependency, Iterable):
-            for d in dependency:
-                self.add(d)
-        else:
-            raise TypeError(type(dependency))  # pragma: no cover
-
-    def __getitem__(self, item):
-        return self.memory[item]
-
-    def remove(self, serial_number):
-        self.memory.pop(serial_number, None)
-
-    def get_args(self):
-        ret = dict()
-        for dependency in self.memory.values():
-            if dependency.arg:
-                ret[dependency.arg] = dependency.id
-        return ret
-
-    def load_snapshot(self):
-        return self.code.service['snapshot'].load(serial_number=list(self.memory.keys()))
-
-    def check_delivery(self):
-        return self.code.service['delivery'].check(serial_number=[k for k, v in self.memory.items() if v.arg])
-
-    def validate_snapshot(self, snapshot):
-        for s in snapshot:
-            if s['state'] == 'ERROR':
-                return DependencyState.ERROR
-            elif s['state'] != 'TERMINATED':
-                return DependencyState.PENDING
-        if len(self) != len(snapshot):
-            return DependencyState.PENDING
-        return DependencyState.RESOLVED
-
-    def validate_delivery(self, snapshot, delivery):
-        if len(self.get_args()) != len(delivery):
-            return DependencyState.PENDING
-        s = {x['_id']: x for x in snapshot}
-        for d in delivery:
-            serial_number = d['_id']
-            if serial_number not in s:
-                return DependencyState.PENDING
-            elif 'timestamp' not in d or 'timestamp' not in s[serial_number]:
-                return DependencyState.PENDING
-            elif d['timestamp'] != s[serial_number]['timestamp']:
-                return DependencyState.PENDING
-        return DependencyState.RESOLVED
-
-    def get_state(self):
-        snapshot = self.load_snapshot()
-        dependency_state = self.validate_snapshot(snapshot=snapshot)
-        if dependency_state != DependencyState.RESOLVED:
-            return dependency_state
-        delivery = self.check_delivery()
-        return self.validate_delivery(snapshot=snapshot, delivery=delivery)
-
-    def __iter__(self):
-        return self.memory.__iter__()
-
-    def keys(self):
-        return self.memory.keys()
-
-    def values(self):
-        return self.memory.values()
-
-    def items(self):
-        return self.memory.items()
-
-    def __len__(self):
-        return len(self.memory)
diff --git a/codepack/dependency/dependency_state.py b/codepack/dependency/dependency_state.py
deleted file mode 100644
index 21d5ceb..0000000
--- a/codepack/dependency/dependency_state.py
+++ /dev/null
@@ -1,23 +0,0 @@
-from enum import Enum
-
-
-class DependencyState(Enum):
-    RESOLVED = 0
-    PENDING = 1
-    ERROR = 2
-
-    def __str__(self):
-        return self.name  # pragma: no cover
-
-    def __repr__(self):
-        return self.__str__()  # pragma: no cover
-
-    def __eq__(self, other):
-        if isinstance(other, type(self)):
-            return self.name == other.name
-        elif isinstance(other, str):
-            return self.name == other
-        elif isinstance(other, int):
-            return self.name == self.__class__(other).name
-        else:
-            return False  # pragma: no cover
diff --git a/codepack/docker/__init__.py b/codepack/docker/__init__.py
deleted file mode 100644
index f22b9b8..0000000
--- a/codepack/docker/__init__.py
+++ /dev/null
@@ -1,1 +0,0 @@
-from codepack.docker.docker_manager import DockerManager
diff --git a/codepack/docker/docker_manager.py b/codepack/docker/docker_manager.py
deleted file mode 100644
index 4fda1c7..0000000
--- a/codepack/docker/docker_manager.py
+++ /dev/null
@@ -1,130 +0,0 @@
-import os
-from docker.errors import ImageNotFound
-from shutil import rmtree
-from typing import Union
-from docker import DockerClient
-from codepack.interface import Docker
-
-
-class DockerManager:
-    CONTAINER_WORKDIR = '/usr/src/codepack'
-
-    def __init__(self, docker: Union[Docker, DockerClient] = None, path: str = './'):
-        if docker is None:
-            self.docker = Docker()
-        else:
-            self.docker = docker
-        self.path = path
-
-    def get_image(self, image: str):
-        ret = None
-        try:
-            ret = self.docker.images.get(image)
-        except ImageNotFound:
-            ret = self.docker.images.pull(image)
-        finally:
-            return ret
-
-    def pull_image(self, image: str):
-        return self.docker.images.pull(image)
-
-    def push_image(self, image: str):
-        return self.docker.images.push(image)
-
-    def run(self, image: str, **kwargs):
-        return self.docker.containers.run(image=image, volumes=['%s:%s' % (os.path.abspath(self.path), self.CONTAINER_WORKDIR)],
-                                          working_dir=self.CONTAINER_WORKDIR, auto_remove=True, name=id(self), **kwargs)
-
-    @staticmethod
-    def extract_requirements_from_file(path: str):
-        with open(path, 'r') as f:
-            lines = f.readlines()
-        return [line.strip() for line in lines]
-
-    @staticmethod
-    def _collect_requirements_in_dict(r: list, d: dict):
-        for _r in r:
-            tokens = _r.split('==')
-            if len(tokens) == 2:
-                d[tokens[0]] = tokens[1]
-            elif len(tokens) == 1:
-                d[tokens[0]] = None
-            else:
-                raise IndexError(len(tokens))
-
-    @staticmethod
-    def combine_requirements(base: list, additive: list):
-        ret = list()
-        buffer = dict()
-        DockerManager._collect_requirements_in_dict(base, buffer)
-        DockerManager._collect_requirements_in_dict(additive, buffer)
-        for module, version in buffer.items():
-            tmp = module
-            if version:
-                tmp += '==%s' % version
-            ret.append(tmp)
-        return ret
-
-    @staticmethod
-    def make_dockerfile(path: str, base_image: str, args: dict = None, envs: dict = None,
-                        requirements: list = None, pip_options: dict = None):
-        ret = str()
-        if args is None:
-            args = dict()
-        if envs is None:
-            envs = dict()
-        if requirements is None:
-            requirements = list()
-        lines = list()
-        lines.append('FROM %s' % base_image)
-        for k, v in args.items():
-            lines.append('ARG %s %s' % (k, v))
-        for k, v in envs.items():
-            lines.append('ENV %s %s' % (k, v))
-        if len(requirements) > 0:
-            prefix = 'RUN python -m pip install'
-            if pip_options:
-                for k, v in pip_options.items():
-                    if v:
-                        if isinstance(v, list):
-                            for _v in v:
-                                prefix += ' --%s %s' % (k, _v)
-                        elif isinstance(v, str):
-                            prefix += ' --%s %s' % (k, v)
-                        else:
-                            raise TypeError(type(v))
-                    else:
-                        prefix += ' --%s' % k
-            lines.append('%s pip' % prefix)
-            lines.append('%s %s' % (prefix, ' '.join(requirements)))
-        with open(os.path.join(path, 'Dockerfile'), 'w') as f:
-            f.writelines(line + '\n' for line in lines)
-        for line in lines:
-            ret += line + '\n'
-        return ret
-
-    def build_image(self, tag, base_image, args=None, envs=None, requirements=None, pip_options=None, tmp_dir=None):
-        if not tmp_dir:
-            tmp_dir = str(id(self))
-        path = os.path.join(self.path, tmp_dir)
-        new_path = False
-        image = None
-        logs = None
-        try:
-            if self.get_image(base_image) is None:
-                return None, None
-            if not os.path.exists(path):
-                os.makedirs(path)
-                new_path = True
-            self.make_dockerfile(path, base_image=base_image, args=args, envs=envs,
-                                 requirements=requirements, pip_options=pip_options)
-            image, logs = self.docker.images.build(path=path, tag=tag)
-        finally:
-            if new_path:
-                rmtree(path)
-            else:
-                if os.path.exists(os.path.join(path, 'requirements.txt')):
-                    os.remove(os.path.join(path, 'requirements.txt'))
-                if os.path.exists(os.path.join(path, 'Dockerfile')):
-                    os.remove(os.path.join(path, 'Dockerfile'))
-            return image, logs
diff --git a/codepack/employee/__init__.py b/codepack/employee/__init__.py
deleted file mode 100644
index 8000191..0000000
--- a/codepack/employee/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-from codepack.employee.worker import Worker
-from codepack.employee.supervisor import Supervisor
diff --git a/codepack/employee/worker.py b/codepack/employee/worker.py
deleted file mode 100644
index bfad9d0..0000000
--- a/codepack/employee/worker.py
+++ /dev/null
@@ -1,51 +0,0 @@
-import requests
-from codepack import Code
-from codepack.snapshot import CodeSnapshot
-from codepack.employee.supervisor import Supervisor
-from codepack.storage import KafkaStorage
-
-
-class Worker(KafkaStorage):
-    def __init__(self, consumer=None, interval=1, callback=None, supervisor=None, consumer_config=None):
-        KafkaStorage.__init__(self, consumer=consumer, consumer_config=consumer_config)
-        self.interval = interval
-        self.supervisor = supervisor
-        self.callback = callback
-        if self.supervisor and not self.callback:
-            if isinstance(self.supervisor, str) or isinstance(self.supervisor, Supervisor):
-                self.callback = self.inform_supervisor_of_termination
-            else:
-                self.close()
-                raise TypeError(type(self.supervisor))
-        self.register(callback=self.callback)
-
-    def register(self, callback):
-        self.callback = callback
-
-    def start(self):
-        self.consumer.consume(self.work, timeout_ms=int(float(self.interval) * 1000))
-
-    def stop(self):
-        pass
-
-    def work(self, buffer):
-        for tp, msgs in buffer.items():
-            for msg in msgs:
-                code = None
-                try:
-                    snapshot = CodeSnapshot.from_dict(msg.value)
-                    code = Code.from_snapshot(snapshot)
-                    code.register(callback=self.callback)
-                    code(*snapshot.args, **snapshot.kwargs)
-                except Exception as e:
-                    print(e)  # log.error(e)
-                    if code is not None:
-                        code.update_state('ERROR')
-                    continue
-
-    def inform_supervisor_of_termination(self, x):
-        if x['state'] == 'TERMINATED':
-            if isinstance(self.supervisor, str):
-                requests.get(self.supervisor + '/organize/%s' % x['serial_number'])
-            elif isinstance(self.supervisor, Supervisor):
-                self.supervisor.organize(x['serial_number'])
diff --git a/codepack/engine/__init__.py b/codepack/engine/__init__.py
deleted file mode 100644
index d60e52a..0000000
--- a/codepack/engine/__init__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from codepack.engine.engine import Engine
-from codepack.engine.blocking_engine import BlockingEngine
-from codepack.engine.non_blocking_engine import NonBlockingEngine
diff --git a/codepack/engine/blocking_engine.py b/codepack/engine/blocking_engine.py
deleted file mode 100644
index 6e7deb0..0000000
--- a/codepack/engine/blocking_engine.py
+++ /dev/null
@@ -1,20 +0,0 @@
-from codepack.engine import Engine
-import time
-
-
-class BlockingEngine(Engine):
-    def __init__(self, callback, interval=1, config_path=None, snapshot_service=None):
-        super().__init__(callback=callback, interval=interval, config_path=config_path, snapshot_service=snapshot_service)
-
-    def start(self):
-        while True:
-            try:
-                time.sleep(self.interval)
-                self.work()
-            except KeyboardInterrupt:
-                break
-            finally:
-                self.stop()
-
-    def stop(self):
-        pass
diff --git a/codepack/engine/engine.py b/codepack/engine/engine.py
deleted file mode 100644
index 30a8090..0000000
--- a/codepack/engine/engine.py
+++ /dev/null
@@ -1,31 +0,0 @@
-import abc
-from codepack.config import Default
-
-
-class Engine(metaclass=abc.ABCMeta):
-    def __init__(self, callback, interval=1, config_path=None, snapshot_service=None):
-        """initialize instance"""
-        self.callback = callback
-        self.interval = interval
-        self.snapshot_service =\
-            snapshot_service if snapshot_service else Default.get_storage_instance('code_snapshot',
-                                                                                   'snapshot_service',
-                                                                                   config_path=config_path)
-
-    @abc.abstractmethod
-    def start(self):
-        """Start loop"""
-
-    @abc.abstractmethod
-    def stop(self):
-        """Stop loop"""
-
-    def work(self):
-        for x in self.snapshot_service.search(key='state', value='WAITING'):
-            resolved = True
-            for s in self.snapshot_service.load([x['serial_number'] for x in x['dependency']], projection={'state'}):
-                if s['state'] != 'TERMINATED':
-                    resolved = False
-                    break
-            if resolved:
-                self.callback(x)
diff --git a/codepack/engine/non_blocking_engine.py b/codepack/engine/non_blocking_engine.py
deleted file mode 100644
index 77bc7cc..0000000
--- a/codepack/engine/non_blocking_engine.py
+++ /dev/null
@@ -1,14 +0,0 @@
-from codepack.engine import Engine
-from codepack.utils import Looper
-
-
-class NonBlockingEngine(Engine):
-    def __init__(self, callback, interval=1, daemon=True, config_path=None, snapshot_service=None):
-        super().__init__(callback=callback, interval=interval, config_path=config_path, snapshot_service=snapshot_service)
-        self.worker = Looper(self.work, interval=interval, daemon=daemon)
-
-    def start(self):
-        self.worker.start()
-
-    def stop(self):
-        self.worker.stop()
diff --git a/codepack/interface/__init__.py b/codepack/interface/__init__.py
index 0b4ba5d..f75aa63 100644
--- a/codepack/interface/__init__.py
+++ b/codepack/interface/__init__.py
@@ -1,10 +1,9 @@
-from codepack.interface.interface import Interface
-from codepack.interface.sql_interface import SQLInterface
-from codepack.interface.mongodb import MongoDB
-from codepack.interface.mysql import MySQL
-from codepack.interface.mssql import MSSQL
-from codepack.interface.dynamodb import DynamoDB
-from codepack.interface.oracledb import OracleDB
-from codepack.interface.docker import Docker
-from codepack.interface.kafka_consumer import KafkaConsumer
-from codepack.interface.kafka_producer import KafkaProducer
+from .mongodb import MongoDB
+from .mysql import MySQL
+from .mssql import MSSQL
+from .dynamodb import DynamoDB
+from .oracledb import OracleDB
+from .docker import Docker
+from .kafka_consumer import KafkaConsumer
+from .kafka_producer import KafkaProducer
+from .s3 import S3
diff --git a/codepack/interface/docker.py b/codepack/interface/docker.py
index 182d9aa..02825ff 100644
--- a/codepack/interface/docker.py
+++ b/codepack/interface/docker.py
@@ -1,22 +1,23 @@
-from docker import DockerClient
-from codepack.interface import Interface
+from codepack.interface.interface import Interface
+import docker
+from typing import Any, Optional
 
 
 class Docker(Interface):
-    def __init__(self, config=None, *args, **kwargs):
+    def __init__(self, config: Optional[dict] = None, *args: Any, **kwargs: Any) -> None:
         super().__init__(config if config else dict())
         self.connect(*args, **kwargs)
 
-    def connect(self, *args, **kwargs):
-        self.session = DockerClient(*args, **self.config, **kwargs)
+    def connect(self, *args: Any, **kwargs: Any) -> docker.DockerClient:
+        self.session = docker.DockerClient(*args, **self.config, **kwargs)
         self._closed = False
         return self.session
 
-    def __getattr__(self, item):
+    def __getattr__(self, item: str) -> Any:
         assert not self.closed(), "connection is closed"
         return getattr(self.session, item)
 
-    def close(self):
+    def close(self) -> None:
         self.session.close()
         if not self.closed():
             self._closed = True
diff --git a/codepack/interface/dynamodb.py b/codepack/interface/dynamodb.py
index bd6d9db..1a45a4f 100644
--- a/codepack/interface/dynamodb.py
+++ b/codepack/interface/dynamodb.py
@@ -1,27 +1,33 @@
+from codepack.interface.sql_interface import SQLInterface
 import boto3
 from botocore.config import Config
-from codepack.interface import SQLInterface
+from botocore.client import BaseClient
 from boto3.dynamodb.types import TypeDeserializer
+from typing import Optional, Callable, Any
 
 
 class DynamoDB(SQLInterface):
-    def __init__(self, config, *args, **kwargs):
+    def __init__(self, config: dict, *args: Any, **kwargs: Any) -> None:
         super().__init__(config)
         self.td = TypeDeserializer()
         self.connect(*args, **kwargs)
 
-    def connect(self, *args, **kwargs):
-        self.session = boto3.client(config=Config(retries=dict(max_attempts=3)), *args, **self.config, **kwargs)
+    def connect(self, *args: Any, **kwargs: Any) -> BaseClient:
+        if 'config' not in self.config and 'config' not in kwargs:
+            self.config['config'] = Config(retries=dict(max_attempts=3))
+        self.session = boto3.client(*args, **self.config, **kwargs)
         self._closed = False
         return self.session
 
-    def list_tables(self, name):
+    def list_tables(self, name: str) -> list:
         ret = list()
         if self.session is None:
             return ret
         return self.session.list_tables(ExclusiveStartTableName=name)['TableNames']
 
-    def query(self, table, q, columns=None, preprocess=None, preprocess_args=None, preprocess_kwargs=None, dummy_column='dummy'):
+    def query(self, table: str, q: str, columns: Optional[list] = None, preprocess: Optional[Callable] = None,
+              preprocess_args: Optional[tuple] = None, preprocess_kwargs: Optional[dict] = None,
+              dummy_column: str = 'dummy') -> list:
         if preprocess is None:
             preprocess = self.do_nothing
         if preprocess_args is None:
@@ -44,7 +50,9 @@ def query(self, table, q, columns=None, preprocess=None, preprocess_args=None, p
             done = start_key is None
         return items
 
-    def select(self, table, columns=None, preprocess=None, preprocess_args=None, preprocess_kwargs=None, dummy_column='dummy', **kwargs):
+    def select(self, table: str, columns: Optional[list] = None, preprocess: Optional[Callable] = None,
+               preprocess_args: Optional[tuple] = None, preprocess_kwargs: Optional[dict] = None,
+               dummy_column: str = 'dummy', **kwargs: Any) -> list:
         q = str()
         if len(kwargs) > 0:
             q += self.encode_sql(**kwargs)
@@ -52,11 +60,11 @@ def select(self, table, columns=None, preprocess=None, preprocess_args=None, pre
                           preprocess=preprocess, preprocess_args=preprocess_args, preprocess_kwargs=preprocess_kwargs,
                           dummy_column=dummy_column)
 
-    def describe_table(self, table):
+    def describe_table(self, table: str) -> dict:
         return self.session.describe_table(TableName=table)['Table']
 
     @staticmethod
-    def array_parser(s, sep='\x7f', dtype=str):
+    def array_parser(s: Any, sep: str = '\x7f', dtype: type = str) -> Any:
         if type(s) == str:
             tmp = s.split(sep)
             ret = [dtype(i) for i in tmp]
@@ -67,9 +75,9 @@ def array_parser(s, sep='\x7f', dtype=str):
         else:
             return s
 
-    def close(self):
+    def close(self) -> None:
         self._closed = True
 
     @staticmethod
-    def do_nothing(x, *args, **kwargs):
+    def do_nothing(x: Any, *args: Any, **kwargs: Any) -> Any:
         return x
diff --git a/codepack/interface/interface.py b/codepack/interface/interface.py
index d0b3653..90b3c64 100644
--- a/codepack/interface/interface.py
+++ b/codepack/interface/interface.py
@@ -1,11 +1,12 @@
+from codepack.config.config import Config
 import abc
-from sshtunnel import SSHTunnelForwarder
-from codepack.config import Config
+import sshtunnel
 from copy import deepcopy
+from typing import Any, Union
 
 
 class Interface(metaclass=abc.ABCMeta):
-    def __init__(self, config):
+    def __init__(self, config: dict) -> None:
         self.config = None
         self.ssh_config = None
         self.ssh = None
@@ -13,12 +14,13 @@ def __init__(self, config):
         self._closed = True
         self.init_config(config)
 
-    def init_config(self, config):
+    def init_config(self, config: dict) -> None:
         _config = deepcopy(config)
         if _config and 'sshtunnel' in _config:
             _ssh_config = _config.pop('sshtunnel')
             if isinstance(_ssh_config, str):
-                config_path, section = _ssh_config.split(':')
+                _config_path, section = _ssh_config.split(':')
+                config_path = Config.get_config_path(path=_config_path)
                 self.ssh_config = Config.parse_config(section=section, config_path=config_path)
             elif isinstance(_ssh_config, dict):
                 self.ssh_config = _ssh_config
@@ -27,26 +29,26 @@ def init_config(self, config):
         self.config = _config
 
     @abc.abstractmethod
-    def connect(self, *args, **kwargs):
+    def connect(self, *args: Any, **kwargs: Any) -> Any:
         """connect to the server"""
 
     @abc.abstractmethod
-    def close(self):
+    def close(self) -> None:
         """close the connection to the server"""
 
-    def closed(self):
+    def closed(self) -> bool:
         return self._closed
 
     @staticmethod
-    def exclude_keys(d, keys):
+    def exclude_keys(d: dict, keys: Union[list, set, dict]) -> dict:
         return {k: v for k, v in d.items() if k not in keys}
 
-    def bind(self, host, port):
+    def bind(self, host: str, port: Union[str, int]) -> tuple:
         if self.ssh_config:
             _ssh_config = self.exclude_keys(self.ssh_config, keys=['ssh_host', 'ssh_port'])
-            self.ssh = SSHTunnelForwarder((self.ssh_config['ssh_host'], int(self.ssh_config['ssh_port'])),
-                                          remote_bind_address=(host, int(port)),
-                                          **_ssh_config)
+            self.ssh = sshtunnel.SSHTunnelForwarder((self.ssh_config['ssh_host'], int(self.ssh_config['ssh_port'])),
+                                                    remote_bind_address=(host, int(port)),
+                                                    **_ssh_config)
             self.ssh.start()
             _host = '127.0.0.1'
             _port = self.ssh.local_bind_port
@@ -56,7 +58,7 @@ def bind(self, host, port):
         return _host, int(_port)
 
     @staticmethod
-    def eval_bool(source):
+    def eval_bool(source: Union[str, bool]) -> bool:
         assert source in ['True', 'False', True, False], "'source' should be either 'True' or 'False'"
         if type(source) == str:
             return eval(source)
diff --git a/codepack/interface/kafka_consumer.py b/codepack/interface/kafka_consumer.py
index ed3e9e1..02faa47 100644
--- a/codepack/interface/kafka_consumer.py
+++ b/codepack/interface/kafka_consumer.py
@@ -1,40 +1,52 @@
-from kafka import KafkaConsumer as Kafka
-from codepack.interface import Interface
+from codepack.interface.interface import Interface
+from codepack.utils import Looper
+import kafka
 from copy import deepcopy
 import json
+from typing import Any, Optional, Callable
 
 
 class KafkaConsumer(Interface):
-    def __init__(self, config, *args, **kwargs):
+    def __init__(self, config: dict, *args: Any, **kwargs: Any) -> None:
         super().__init__(config)
         self.connect(*args, **kwargs)
+        self.looper = None
 
-    def connect(self, *args, **kwargs):
+    def connect(self, *args: Any, **kwargs: Any) -> kafka.KafkaConsumer:
         if 'value_deserializer' not in self.config and 'value_deserializer' not in kwargs:
             kwargs['value_deserializer'] = lambda x: json.loads(x.decode('utf-8'))
         if 'topic' in self.config:
             _config = deepcopy(self.config)
             _topic = _config.pop('topic')
-            self.session = Kafka(_topic, *args, **_config, **kwargs)
+            self.session = kafka.KafkaConsumer(_topic, *args, **_config, **kwargs)
         else:
-            self.session = Kafka(*args, **self.config, **kwargs)
+            self.session = kafka.KafkaConsumer(*args, **self.config, **kwargs)
         self._closed = False
         return self.session
 
-    def __getattr__(self, item):
+    def __getattr__(self, item: str) -> Any:
         assert not self.closed(), "connection is closed"
         return getattr(self.session, item)
 
-    def consume(self, callback, *args, **kwargs):
-        while True:
-            try:
-                buffer = self.session.poll(*args, **kwargs)
-                if buffer:
-                    callback(buffer)
-            except KeyboardInterrupt:
-                break
+    def consume(self, callback: Callable, background: bool = False, *args: Any, **kwargs: Any) -> None:
+        assert not self.closed(), "connection is closed"
+        self.stop()
+        self.looper = Looper(func=self.fetch, interval=0, background=background, callback=callback, *args, **kwargs)
+        self.looper.start()
+
+    def stop(self):
+        if self.looper:
+            self.looper.stop()
+
+    def fetch(self, callback: Callable, *args: Any, **kwargs: Any) -> Optional[Any]:
+        buffer = self.session.poll(*args, **kwargs)
+        if buffer:
+            return callback(buffer)
+        else:
+            return None
 
-    def close(self):
+    def close(self) -> None:
+        self.stop()
         self.session.close()
         if not self.closed():
             if self.ssh_config and self.ssh is not None:
diff --git a/codepack/interface/kafka_producer.py b/codepack/interface/kafka_producer.py
index 7096530..3ea8d78 100644
--- a/codepack/interface/kafka_producer.py
+++ b/codepack/interface/kafka_producer.py
@@ -1,30 +1,31 @@
-from kafka import KafkaProducer as Kafka
-from codepack.interface import Interface
-from copy import deepcopy
+from codepack.interface.interface import Interface
+import kafka
 import json
+from typing import Any
 
 
 class KafkaProducer(Interface):
-    def __init__(self, config, *args, **kwargs):
+    def __init__(self, config: dict, *args: Any, **kwargs: Any) -> None:
         super().__init__(config)
         self.connect(*args, **kwargs)
 
-    def connect(self, *args, **kwargs):
+    def connect(self, *args: Any, **kwargs: Any) -> kafka.KafkaProducer:
         if 'value_serializer' not in self.config and 'value_serializer' not in kwargs:
             kwargs['value_serializer'] = lambda x: json.dumps(x).encode('utf-8')
-        self.session = Kafka(*args, **self.config, **kwargs)
+        self.session = kafka.KafkaProducer(**self.config, **kwargs)
         self._closed = False
         return self.session
 
-    def __getattr__(self, item):
+    def __getattr__(self, item: str) -> Any:
         assert not self.closed(), "connection is closed"
         return getattr(self.session, item)
 
-    def produce(self, *args, **kwargs):
+    def produce(self, *args: Any, **kwargs: Any) -> None:
+        assert not self.closed(), "connection is closed"
         self.session.send(*args, **kwargs)
         self.session.flush()
 
-    def close(self):
+    def close(self) -> None:
         self.session.close()
         if not self.closed():
             if self.ssh_config and self.ssh is not None:
diff --git a/codepack/interface/mongodb.py b/codepack/interface/mongodb.py
index b79f5c2..5fa319c 100644
--- a/codepack/interface/mongodb.py
+++ b/codepack/interface/mongodb.py
@@ -1,30 +1,31 @@
-from pymongo import MongoClient
-from codepack.interface import Interface
+from codepack.interface.interface import Interface
+import pymongo
+from typing import Any
 
 
 class MongoDB(Interface):
-    def __init__(self, config, *args, **kwargs):
+    def __init__(self, config: dict, *args: Any, **kwargs: Any) -> None:
         super().__init__(config)
         self.connect(*args, **kwargs)
 
-    def connect(self, *args, **kwargs):
+    def connect(self, *args: Any, **kwargs: Any) -> pymongo.mongo_client.MongoClient:
         host, port = self.bind(host=self.config['host'], port=self.config['port'])
         _config = self.exclude_keys(self.config, keys=['host', 'port'])
         if 'connect' in _config:
             _config['connect'] = self.eval_bool(_config['connect'])
-        self.session = MongoClient(host=host, port=port, *args, **_config, **kwargs)
+        self.session = pymongo.MongoClient(host=host, port=port, *args, **_config, **kwargs)
         self._closed = False
         return self.session
 
-    def __getitem__(self, item):
+    def __getitem__(self, item: str) -> Any:
         assert not self.closed(), "connection is closed"
         return self.session[item]
 
-    def __getattr__(self, item):
+    def __getattr__(self, item: str) -> Any:
         assert not self.closed(), "connection is closed"
         return self.__getitem__(item)
 
-    def close(self):
+    def close(self) -> None:
         self.session.close()
         if not self.closed():
             if self.ssh_config and self.ssh is not None:
diff --git a/codepack/interface/mssql.py b/codepack/interface/mssql.py
index 09a7217..8ba0ea7 100644
--- a/codepack/interface/mssql.py
+++ b/codepack/interface/mssql.py
@@ -1,14 +1,15 @@
+from codepack.interface.sql_interface import SQLInterface
 import pymssql
-from codepack.interface import SQLInterface
+from typing import Any, Union, Optional
 
 
 class MSSQL(SQLInterface):
-    def __init__(self, config, *args, **kwargs):
+    def __init__(self, config: dict, *args: Any, **kwargs: Any) -> None:
         super().__init__(config)
         self.as_dict = None
         self.connect(*args, **kwargs)
 
-    def connect(self, *args, **kwargs):
+    def connect(self, *args: Any, **kwargs: Any) -> pymssql.Connection:
         host, port = self.bind(host=self.config['host'], port=self.config['port'])
         _config = self.exclude_keys(self.config, keys=['host', 'port'])
         self.as_dict = False
@@ -21,7 +22,7 @@ def connect(self, *args, **kwargs):
         self._closed = False
         return self.session
 
-    def query(self, q, commit=False):
+    def query(self, q: Union[str, list], commit: bool = False) -> Optional[list]:
         assert not self.closed(), "connection is closed"
         columns = None
         rows = None
@@ -50,12 +51,12 @@ def query(self, q, commit=False):
             else:
                 return None
 
-    def insert(self, db, table, commit=False, **kwargs):
+    def insert(self, db: str, table: str, commit: bool = False, **kwargs: Any) -> None:
         q = self.make_insert_query(db=db, table=table, **kwargs)
         self.query(q, commit=commit)
 
     @staticmethod
-    def make_insert_query(db, table, **kwargs):
+    def make_insert_query(db: str, table: str, **kwargs: Any) -> str:
         columns = kwargs.keys()
         tmp = [kwargs[c] for c in columns]
         values = list()
@@ -70,14 +71,15 @@ def make_insert_query(db, table, **kwargs):
         q += "values (%s)" % ", ".join(values)
         return q
 
-    def insert_many(self, db, table, rows, commit=False):
+    def insert_many(self, db: str, table: str, rows: list, commit: bool = False) -> None:
         qs = list()
         for row in rows:
             qs.append(self.make_insert_query(db=db, table=table, **row))
         self.query(qs, commit=commit)
 
     @staticmethod
-    def make_select_query(db, table, projection=None, tx_isolation=None, **kwargs):
+    def make_select_query(db: str, table: str, projection: Optional[Union[str, list]] = None,
+                          tx_isolation: Optional[str] = None, **kwargs: Any) -> str:
         projection_token = '*'
         if projection:
             if type(projection) == list:
@@ -92,16 +94,17 @@ def make_select_query(db, table, projection=None, tx_isolation=None, **kwargs):
             q += " with (%s)" % tx_isolation
         return q
 
-    def select(self, db, table, projection=None, tx_isolation=None, **kwargs):
+    def select(self, db: str, table: str, projection: Optional[Union[str, list]] = None,
+               tx_isolation: Optional[str] = None, **kwargs: Any) -> list:
         q = self.make_select_query(db=db, table=table, projection=projection, tx_isolation=tx_isolation, **kwargs)
         return self.query(q)
 
-    def exec(self, db, procedure, **kwargs):
+    def exec(self, db: str, procedure: str, **kwargs: Any) -> list:
         q = self.make_exec_query(db, procedure, **kwargs)
         return self.query(q)
 
     @staticmethod
-    def make_exec_query(db, procedure, **kwargs):
+    def make_exec_query(db: str, procedure: str, **kwargs: Any) -> str:
         q = 'exec %s.%s' % (db, procedure)
         tokens = list()
         for k, v in kwargs.items():
@@ -110,7 +113,7 @@ def make_exec_query(db, procedure, **kwargs):
             q += ' ' + ', '.join(tokens)
         return q
 
-    def delete(self, db, table, commit=False, **kwargs):
+    def delete(self, db: str, table: str, commit: bool = False, **kwargs: Any) -> Optional[list]:
         q = "delete from %s.%s" % (db, table)
         if len(kwargs) > 0:
             q += " where "
@@ -119,7 +122,7 @@ def delete(self, db, table, commit=False, **kwargs):
         else:
             return None
 
-    def close(self):
+    def close(self) -> None:
         if not self.closed():
             self.session.close()
             if self.ssh_config and self.ssh is not None:
diff --git a/codepack/interface/mysql.py b/codepack/interface/mysql.py
index 3da708f..5ee6d75 100644
--- a/codepack/interface/mysql.py
+++ b/codepack/interface/mysql.py
@@ -1,14 +1,15 @@
+from codepack.interface.sql_interface import SQLInterface
 import pymysql
-from codepack.interface import SQLInterface
 import re
+from typing import Any, Union, Optional
 
 
 class MySQL(SQLInterface):
-    def __init__(self, config, *args, **kwargs):
+    def __init__(self, config: dict, *args: Any, **kwargs: Any) -> None:
         super().__init__(config)
         self.connect(*args, **kwargs)
 
-    def connect(self, *args, **kwargs):
+    def connect(self, *args: Any, **kwargs: Any) -> pymysql.connections.Connection:
         host, port = self.bind(host=self.config['host'], port=self.config['port'])
         _config = self.exclude_keys(self.config, keys=['host', 'port'])
         if 'cursorclass' in _config:
@@ -18,7 +19,7 @@ def connect(self, *args, **kwargs):
         return self.session
 
     @staticmethod
-    def eval_cursor_object(cursorclass):
+    def eval_cursor_object(cursorclass: Union[str, pymysql.cursors.Cursor]) -> pymysql.cursors.Cursor:
         if type(cursorclass) == str:
             pat = re.compile("\A(pymysql)[.]cursors[.][^.]*(Cursor)\Z")
             assert pat.match(cursorclass), "'cursorclass' should be one of the cursor objects of pymysql"
@@ -26,13 +27,13 @@ def eval_cursor_object(cursorclass):
         else:
             return cursorclass
 
-    def commit(self):
+    def commit(self) -> None:
         self.session.commit()
 
-    def rollback(self):
+    def rollback(self) -> None:
         self.session.rollback()
 
-    def query(self, q, commit=False):
+    def query(self, q: Union[str, list], commit: bool = False) -> Optional[list]:
         assert not self.closed(), "connection is closed"
         columns = None
         rows = None
@@ -49,9 +50,9 @@ def query(self, q, commit=False):
             rows = cursor.fetchall()
             cursor.close()
             if commit:
-                self.session.commit()
+                self.commit()
         except Exception as e:
-            self.session.rollback()
+            self.rollback()
             raise e
         if cursorclass in [pymysql.cursors.DictCursor, pymysql.cursors.SSDictCursor]:
             return rows
@@ -62,7 +63,7 @@ def query(self, q, commit=False):
                 return None
 
     @staticmethod
-    def make_insert_query(db, table, update=True, **kwargs):
+    def make_insert_query(db: str, table: str, update: bool = True, **kwargs: Any) -> str:
         columns = kwargs.keys()
         tmp = [kwargs[c] for c in columns]
         values = list()
@@ -81,18 +82,18 @@ def make_insert_query(db, table, update=True, **kwargs):
         q += "values (%s)" % ", ".join(values)
         return q
 
-    def insert(self, db, table, update=True, commit=False, **kwargs):
+    def insert(self, db: str, table: str, update: bool = True, commit: bool = False, **kwargs: Any) -> None:
         q = self.make_insert_query(db=db, table=table, update=update, **kwargs)
         self.query(q, commit=commit)
 
-    def insert_many(self, db, table, rows, update=True, commit=False):
+    def insert_many(self, db: str, table: str, rows: list, update: bool = True, commit: bool = False) -> None:
         qs = list()
         for row in rows:
             qs.append(self.make_insert_query(db=db, table=table, update=update, **row))
         self.query(qs, commit=commit)
 
     @staticmethod
-    def make_select_query(db, table, projection=None, **kwargs):
+    def make_select_query(db: str, table: str, projection: Optional[Union[str, list]] = None, **kwargs: Any) -> str:
         projection_token = '*'
         if projection:
             if type(projection) == list:
@@ -105,11 +106,11 @@ def make_select_query(db, table, projection=None, **kwargs):
             q += MySQL.encode_sql(**kwargs)
         return q
 
-    def select(self, db, table, projection=None, **kwargs):
+    def select(self, db: str, table: str, projection: Optional[Union[str, list]] = None, **kwargs: Any) -> list:
         q = self.make_select_query(db=db, table=table, projection=projection, **kwargs)
         return self.query(q)
 
-    def delete(self, db, table, commit=False, **kwargs):
+    def delete(self, db: str, table: str, commit: bool = False, **kwargs: Any) -> Optional[list]:
         q = "delete from %s.%s" % (db, table)
         if len(kwargs) > 0:
             q += " where "
@@ -118,12 +119,12 @@ def delete(self, db, table, commit=False, **kwargs):
         else:
             return None
 
-    def call(self, db, procedure, **kwargs):
+    def call(self, db: str, procedure: str, **kwargs: Any) -> list:
         q = self.make_call_query(db, procedure, **kwargs)
         return self.query(q)
 
     @staticmethod
-    def make_call_query(db, procedure, **kwargs):
+    def make_call_query(db: str, procedure: str, **kwargs: Any) -> str:
         q = 'call %s.%s' % (db, procedure)
         tokens = list()
         for k, v in kwargs.items():
@@ -132,7 +133,7 @@ def make_call_query(db, procedure, **kwargs):
             q += '(%s)' % ', '.join(tokens)
         return q
 
-    def close(self):
+    def close(self) -> None:
         if not self.closed():
             self.session.close()
             if self.ssh_config and self.ssh is not None:
diff --git a/codepack/interface/oracledb.py b/codepack/interface/oracledb.py
index 4a89de1..f45637a 100644
--- a/codepack/interface/oracledb.py
+++ b/codepack/interface/oracledb.py
@@ -1,21 +1,22 @@
+from codepack.interface.sql_interface import SQLInterface
 import cx_Oracle
-from codepack.interface import SQLInterface
 from functools import partial
+from typing import Any, Union, Optional
 
 
-def make_named_row(names, *args):
+def make_named_row(names: list, *args: Any) -> dict:
     if len(names) != len(args):
         raise Exception('len(names) != len(args)')
     return dict(zip(names, args))
 
 
 class OracleDB(SQLInterface):
-    def __init__(self, config, *args, **kwargs):
+    def __init__(self, config: dict, *args: Any, **kwargs: Any) -> None:
         super().__init__(config)
         self.as_dict = None
         self.connect(*args, **kwargs)
 
-    def connect(self, *args, **kwargs):
+    def connect(self, *args: Any, **kwargs: Any) -> cx_Oracle.Connection:
         host, port = self.bind(host=self.config['host'], port=self.config['port'])
         exclude_keys = ['host', 'port']
         if 'service_name' in self.config:
@@ -35,7 +36,7 @@ def connect(self, *args, **kwargs):
         self._closed = False
         return self.session
 
-    def query(self, q, commit=False):
+    def query(self, q: Union[str, list], commit: bool = False) -> Optional[list]:
         assert not self.closed(), "connection is closed"
         columns = None
         rows = None
@@ -65,7 +66,7 @@ def query(self, q, commit=False):
             else:
                 return None
 
-    def close(self):
+    def close(self) -> None:
         if not self.closed():
             self.session.close()
             if self.ssh_config and self.ssh is not None:
diff --git a/codepack/interface/s3.py b/codepack/interface/s3.py
new file mode 100644
index 0000000..992e6d6
--- /dev/null
+++ b/codepack/interface/s3.py
@@ -0,0 +1,57 @@
+from codepack.interface.interface import Interface
+import boto3
+from botocore.config import Config
+from botocore.client import BaseClient
+from botocore.response import StreamingBody
+from typing import Any, Union, Optional
+
+
+class S3(Interface):
+    def __init__(self, config: dict, *args: Any, **kwargs: Any) -> None:
+        super().__init__(config)
+        self.connect(*args, **kwargs)
+
+    def connect(self, *args: Any, **kwargs: Any) -> BaseClient:
+        if 'config' not in self.config and 'config' not in kwargs:
+            self.config['config'] = Config(retries=dict(max_attempts=3))
+        self.session = boto3.client(*args, **self.config, **kwargs)
+        self._closed = False
+        return self.session
+
+    def list_objects(self, bucket: str, prefix: str = '', *args: Any, **kwargs: Any) -> list:
+        ret = list()
+        if self.session is None:
+            return ret
+        for page in self.session.get_paginator('list_objects').paginate(Bucket=bucket, Prefix=prefix, *args, **kwargs):
+            ret += page.get('Contents', list())
+        return ret
+
+    def download(self, bucket: str, key: str,
+                 streaming: bool = False, *args: Any, **kwargs: Any) -> Optional[Union[bytes, StreamingBody]]:
+        try:
+            obj = self.session.get_object(Bucket=bucket, Key=key, *args, **kwargs)
+        except self.session.exceptions.NoSuchKey:
+            return None
+        body = obj.get('Body', None)
+        if streaming:
+            return body
+        elif body:
+            return body.read()
+        else:
+            return None
+
+    def exist(self, bucket: str, key: str, *args: Any, **kwargs: Any) -> bool:
+        try:
+            obj = self.session.head_object(Bucket=bucket, Key=key, *args, **kwargs)
+            return obj is not None
+        except self.session.exceptions.ClientError:
+            return False
+
+    def delete(self, bucket: str, key: str, *args: Any, **kwargs: Any) -> dict:
+        return self.session.delete_object(Bucket=bucket, Key=key, *args, **kwargs)
+
+    def upload(self, bucket: str, key: str, data: Union[str, bytes], *args: Any, **kwargs: Any) -> dict:
+        return self.session.put_object(Bucket=bucket, Key=key, Body=data, *args, **kwargs)
+
+    def close(self) -> None:
+        self._closed = True
diff --git a/codepack/interface/sql_interface.py b/codepack/interface/sql_interface.py
index f1edb98..3ade7a8 100644
--- a/codepack/interface/sql_interface.py
+++ b/codepack/interface/sql_interface.py
@@ -1,11 +1,12 @@
+from codepack.interface.interface import Interface
 import abc
 import numpy as np
-from codepack.interface import Interface
+from typing import Optional, Any
 
 
 class SQLInterface(Interface, metaclass=abc.ABCMeta):
     @staticmethod
-    def isnan(value):
+    def isnan(value: Any) -> bool:
         ret = False
         try:
             ret = np.isnan(value)
@@ -15,14 +16,15 @@ def isnan(value):
             return ret
 
     @staticmethod
-    def _enclose(token, mark="'", apply=True):
+    def _enclose(token: str, mark: str = "'", apply: bool = True) -> str:
         if apply:
             return mark + token + mark
         else:
             return token
 
     @staticmethod
-    def encode_sql(custom_op=None, default_joint='and', custom_joint='and', **kwargs):
+    def encode_sql(custom_op: Optional[dict] = None,
+                   default_joint: str = 'and', custom_joint: str = 'and', **kwargs: Any) -> str:
         tokens = list()
         for k, v in kwargs.items():
             if type(v) is list:
@@ -33,7 +35,6 @@ def encode_sql(custom_op=None, default_joint='and', custom_joint='and', **kwargs
                 quote = True if type(v) == str else False
                 template = k + ' = %s' % SQLInterface._enclose('%s', mark="'", apply=quote)
                 tokens.append(template % v)
-
         if custom_op:
             for k, v in custom_op.items():
                 custom_tokens = list()
diff --git a/codepack/plugin/__init__.py b/codepack/plugin/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/codepack/plugin/callback_service.py b/codepack/plugin/callback_service.py
new file mode 100644
index 0000000..e7471c1
--- /dev/null
+++ b/codepack/plugin/callback_service.py
@@ -0,0 +1,30 @@
+from codepack.plugin.service import Service
+from codepack.callback.callback import Callback
+from collections.abc import Callable
+from typing import TypeVar, Union, Optional
+
+
+Storage = TypeVar('Storage', bound='codepack.storage.storage.Storage')
+Storable = TypeVar('Storable', bound='codepack.storage.storable.Storable')
+
+
+class CallbackService(Service):
+    def __init__(self, storage: Storage) -> None:
+        super().__init__(storage=storage)
+        if self.storage.key != 'id':
+            self.storage.key = 'id'
+
+    def push(self, callback: Callable, context: Optional[dict] = None) -> str:
+        cb = Callback(function=callback, context=context)
+        self.storage.save(cb, update=True)
+        return cb.id
+
+    def pull(self, name: Union[str, list]) -> Optional[Union[Storable, dict, list]]:
+        cb = self.storage.load(key=name)
+        return cb
+
+    def remove(self, name: Union[str, list]) -> None:
+        self.storage.remove(key=name)
+
+    def exist(self, name: Union[str, list]) -> Union[bool, list]:
+        return self.storage.exist(key=name)
diff --git a/codepack/plugin/delivery_service.py b/codepack/plugin/delivery_service.py
new file mode 100644
index 0000000..d5bb2cc
--- /dev/null
+++ b/codepack/plugin/delivery_service.py
@@ -0,0 +1,26 @@
+from codepack.plugin.service import Service
+from typing import Union, TypeVar, Optional, Any
+
+
+Storage = TypeVar('Storage', bound='codepack.storage.storage.Storage')
+
+
+class DeliveryService(Service):
+    def __init__(self, storage: Storage) -> None:
+        super().__init__(storage=storage)
+        if self.storage.key != 'serial_number':
+            self.storage.key = 'serial_number'
+
+    def send(self, id: str, serial_number: str, item: Optional[Any] = None,
+             timestamp: Optional[float] = None) -> None:
+        item = self.storage.item_type(id=id, serial_number=serial_number, item=item, timestamp=timestamp)
+        self.storage.save(item=item, update=True)
+
+    def receive(self, serial_number: str) -> Any:
+        return self.storage.load(key=serial_number).receive()
+
+    def check(self, serial_number: Union[str, list]) -> Union[bool, list]:
+        return self.storage.exist(key=serial_number, summary='and')
+
+    def cancel(self, serial_number: Union[str, list]) -> None:
+        self.storage.remove(key=serial_number)
diff --git a/codepack/plugin/dependency_bag.py b/codepack/plugin/dependency_bag.py
new file mode 100644
index 0000000..c2397e1
--- /dev/null
+++ b/codepack/plugin/dependency_bag.py
@@ -0,0 +1,76 @@
+from codepack.storage.memory_storage import MemoryStorage
+from codepack.dependency.dependency import Dependency
+from collections.abc import Iterable
+from typing import Union, Optional, Type, TypeVar, KeysView, ValuesView, ItemsView, Iterator
+
+
+Code = TypeVar('Code', bound='codepack.code.Code')
+CodeSnapshot = TypeVar('CodeSnapshot', bound='codepack.snapshot.code_snapshot.CodeSnapshot')
+
+
+class DependencyBag(MemoryStorage, Iterable):
+    def __init__(self, code: Code, item_type: Type[Dependency] = Dependency) -> None:
+        MemoryStorage.__init__(self, item_type=item_type)
+        self.code = code
+
+    def add(self, dependency: Union[Dependency, dict, Iterable]) -> None:
+        if isinstance(dependency, Dependency):
+            self.code.assert_arg(dependency.arg)
+            self.memory[dependency.serial_number] = dependency
+        elif isinstance(dependency, dict):
+            self.code.assert_arg(dependency['arg'])
+            d = Dependency.from_dict(d=dependency)
+            d.bind(self.code)
+            self.memory[dependency['serial_number']] = d
+        elif isinstance(dependency, Iterable):
+            for d in dependency:
+                self.add(d)
+        else:
+            raise TypeError(type(dependency))  # pragma: no cover
+
+    def __getitem__(self, item: str) -> Dependency:
+        return self.memory[item]
+
+    def remove(self, serial_number: str) -> None:
+        self.memory.pop(serial_number, None)
+
+    def get_args(self) -> dict:
+        ret = dict()
+        for dependency in self.memory.values():
+            if dependency.arg:
+                ret[dependency.arg] = dependency.id
+        return ret
+
+    def load_snapshot(self) -> Optional[Union[CodeSnapshot, dict, list]]:
+        return self.code.service['snapshot'].load(serial_number=list(self.memory.keys()))
+
+    def check_delivery(self) -> Union[bool, list]:
+        return self.code.service['delivery'].check(serial_number=[k for k, v in self.memory.items() if v.arg])
+
+    def validate(self, snapshot: list) -> str:
+        for s in snapshot:
+            if s['state'] != 'TERMINATED':
+                return 'WAITING'
+        if len(self) != len(snapshot):
+            return 'WAITING'
+        if not self.check_delivery():
+            return 'ERROR'
+        return 'READY'
+
+    def get_state(self) -> str:
+        return self.validate(snapshot=self.load_snapshot())
+
+    def __iter__(self) -> Iterator:
+        return self.memory.__iter__()
+
+    def keys(self) -> KeysView:
+        return self.memory.keys()
+
+    def values(self) -> ValuesView:
+        return self.memory.values()
+
+    def items(self) -> ItemsView:
+        return self.memory.items()
+
+    def __len__(self) -> int:
+        return len(self.memory)
diff --git a/codepack/plugin/dependency_monitor.py b/codepack/plugin/dependency_monitor.py
new file mode 100644
index 0000000..05031ef
--- /dev/null
+++ b/codepack/plugin/dependency_monitor.py
@@ -0,0 +1,34 @@
+from codepack.config.default import Default
+from codepack.utils.looper import Looper
+from typing import Callable, Optional, TypeVar
+
+
+SnapshotService = TypeVar('SnapshotService', bound='codepack.plugin.snapshot_service.SnapshotService')
+
+
+class DependencyMonitor:
+    def __init__(self, callback: Callable, interval: float = 10, background: bool = False,
+                 daemon: bool = True, config_path: Optional[str] = None,
+                 snapshot_service: Optional[SnapshotService] = None) -> None:
+        self.callback = callback
+        self.interval = interval
+        self.snapshot_service =\
+            snapshot_service if snapshot_service else Default.get_service('code_snapshot', 'snapshot_service',
+                                                                          config_path=config_path)
+        self.looper = Looper(self.monitor, interval=interval, background=background, daemon=daemon)
+
+    def start(self) -> None:
+        self.looper.start()
+
+    def stop(self) -> None:
+        self.looper.stop()
+
+    def monitor(self) -> None:
+        for x in self.snapshot_service.search(key='state', value='WAITING'):
+            resolved = True
+            for s in self.snapshot_service.load([x['serial_number'] for x in x['dependency']], projection={'state'}):
+                if s['state'] != 'TERMINATED':
+                    resolved = False
+                    break
+            if resolved:
+                self.callback(x)
diff --git a/codepack/plugin/docker_manager.py b/codepack/plugin/docker_manager.py
new file mode 100644
index 0000000..6b76df7
--- /dev/null
+++ b/codepack/plugin/docker_manager.py
@@ -0,0 +1,162 @@
+from codepack.plugin.manager import Manager
+from codepack.config.config import Config
+from codepack.interface.docker import Docker
+import os
+from docker.errors import ImageNotFound
+from shutil import rmtree
+from typing import Any, Union, Optional, TypeVar, Generator
+from docker import DockerClient
+import json
+
+
+Image = TypeVar('Image', bound='docker.models.images.Image')
+Model = TypeVar('Model', bound='docker.models.resource.Model')
+
+
+class DockerManager(Manager):
+    CONTAINER_WORK_DIR = '/usr/src/codepack'
+    CONTAINER_LOG_DIR = '/usr/logs'
+
+    def __init__(self, docker: Optional[Union[Docker, DockerClient, dict]] = None,
+                 path: str = './', run_opt: Optional[str] = None) -> None:
+        if docker is None:
+            self.docker = Docker()
+        elif isinstance(docker, dict):
+            self.docker = Docker(config=docker)
+        else:
+            self.docker = docker
+        self.path = path
+        if run_opt:
+            self.run_opt = json.loads(run_opt)
+        else:
+            self.run_opt = dict()
+
+    def get_image(self, image: str) -> Image:
+        ret = None
+        try:
+            ret = self.docker.images.get(image)
+        except ImageNotFound:
+            ret = self.docker.images.pull(image)
+        finally:
+            return ret
+
+    def pull_image(self, image: str) -> Image:
+        return self.docker.images.pull(image)
+
+    def push_image(self, image: str) -> Union[Generator, str]:
+        return self.docker.images.push(image)
+
+    def run(self, image: str, command: Optional[Union[str, list]] = None,
+            path: Optional[str] = None, volumes: Optional[list] = None, environment: Optional[list] = None,
+            **kwargs: Any) -> Union[Model, None, bytes]:
+        if volumes is None:
+            volumes = list()
+        if environment is None:
+            environment = list()
+        _path = path if path else self.path
+        return self.docker.containers\
+            .run(image=image, command=command,
+                 volumes=volumes + ['%s:%s' % (os.path.abspath(_path), self.CONTAINER_WORK_DIR),
+                                    '%s:%s' % (os.path.abspath(Config.get_log_dir()), self.CONTAINER_LOG_DIR)],
+                 environment=environment + ['%s=%s' % (Config.LABEL_LOGGER_LOG_DIR, self.CONTAINER_LOG_DIR)],
+                 working_dir=self.CONTAINER_WORK_DIR, auto_remove=True, name=id(self),
+                 **self.run_opt, **kwargs)
+
+    @staticmethod
+    def extract_requirements_from_file(path: str) -> list:
+        with open(path, 'r') as f:
+            lines = f.readlines()
+        return [line.strip() for line in lines]
+
+    @staticmethod
+    def _collect_requirements_in_dict(r: list, d: dict) -> None:
+        for _r in r:
+            tokens = _r.split('==')
+            if len(tokens) == 2:
+                d[tokens[0]] = tokens[1]
+            elif len(tokens) == 1:
+                d[tokens[0]] = None
+            else:
+                raise IndexError(len(tokens))
+
+    @staticmethod
+    def combine_requirements(base: list, additive: list) -> list:
+        ret = list()
+        buffer = dict()
+        DockerManager._collect_requirements_in_dict(base, buffer)
+        DockerManager._collect_requirements_in_dict(additive, buffer)
+        for module, version in buffer.items():
+            tmp = module
+            if version:
+                tmp += '==%s' % version
+            ret.append(tmp)
+        return ret
+
+    @staticmethod
+    def make_dockerfile(base_image: str, path: str = './', args: Optional[dict] = None, envs: Optional[dict] = None,
+                        requirements: Optional[list] = None, pip_options: Optional[dict] = None) -> str:
+        ret = str()
+        if args is None:
+            args = dict()
+        if envs is None:
+            envs = dict()
+        if requirements is None:
+            requirements = list()
+        lines = list()
+        lines.append('FROM %s' % base_image)
+        for k, v in args.items():
+            lines.append('ARG %s %s' % (k, v))
+        for k, v in envs.items():
+            lines.append('ENV %s %s' % (k, v))
+        if len(requirements) > 0:
+            prefix = 'RUN python -m pip install'
+            if pip_options:
+                for k, v in pip_options.items():
+                    if v:
+                        if isinstance(v, list):
+                            for _v in v:
+                                prefix += ' --%s %s' % (k, _v)
+                        elif isinstance(v, str):
+                            prefix += ' --%s %s' % (k, v)
+                        else:
+                            raise TypeError(type(v))
+                    else:
+                        prefix += ' --%s' % k
+            lines.append('%s pip' % prefix)
+            lines.append('%s %s' % (prefix, ' '.join(requirements)))
+        with open(os.path.join(path, 'Dockerfile'), 'w') as f:
+            f.writelines(line + '\n' for line in lines)
+        for line in lines:
+            ret += line + '\n'
+        return ret
+
+    def build_image(self, tag: str, base_image: str, args: Optional[dict] = None, envs: Optional[dict] = None,
+                    requirements: Optional[list] = None, pip_options: Optional[dict] = None,
+                    tmp_dir: Optional[str] = None, path: Optional[str] = None) -> tuple:
+        if not tmp_dir:
+            tmp_dir = str(id(self))
+        _path = os.path.join(path if path else self.path, tmp_dir)
+        new_path = False
+        image = None
+        logs = None
+        try:
+            if self.get_image(base_image) is None:
+                return None, None
+            if not os.path.exists(_path):
+                os.makedirs(_path)
+                new_path = True
+            self.make_dockerfile(base_image=base_image, path=_path, args=args, envs=envs,
+                                 requirements=requirements, pip_options=pip_options)
+            image, logs = self.docker.images.build(path=_path, tag=tag)
+        finally:
+            if new_path:
+                rmtree(_path)
+            else:
+                self.remove_file_if_exists(os.path.join(_path, 'requirements.txt'))
+                self.remove_file_if_exists(os.path.join(_path, 'Dockerfile'))
+            return image, logs
+
+    @staticmethod
+    def remove_file_if_exists(path: str) -> None:
+        if os.path.exists(path):
+            os.remove(path)
diff --git a/codepack/plugin/employee.py b/codepack/plugin/employee.py
new file mode 100644
index 0000000..92d2ed7
--- /dev/null
+++ b/codepack/plugin/employee.py
@@ -0,0 +1,10 @@
+from codepack.storage.messenger import Messenger
+import abc
+
+
+class Employee(metaclass=abc.ABCMeta):
+    def __init__(self, messenger: Messenger) -> None:
+        self.messenger = messenger
+
+    def close(self) -> None:
+        self.messenger.close()
diff --git a/codepack/plugin/interpreter_manager.py b/codepack/plugin/interpreter_manager.py
new file mode 100644
index 0000000..e52efdf
--- /dev/null
+++ b/codepack/plugin/interpreter_manager.py
@@ -0,0 +1,28 @@
+from codepack.plugin.manager import Manager
+import os
+from typing import Any, Union, Optional, TypeVar
+import json
+import subprocess
+
+
+CompletedProcess = TypeVar('CompletedProcess', bound='subprocess.CompletedProcess')
+
+
+class InterpreterManager(Manager):
+    def __init__(self, path: str, run_opt: Optional[str] = None) -> None:
+        self.path = path
+        if run_opt:
+            self.run_opt = json.loads(run_opt)
+        else:
+            self.run_opt = dict()
+
+    def get_env(self, env: str) -> str:
+        return os.path.join(self.path, env, 'bin')
+
+    def run(self, env: str, command: Optional[Union[str, list]] = None, **kwargs: Any) -> CompletedProcess:
+        if len(command) > 0 and 'python' in command[0]:
+            interpreter = os.path.join(self.get_env(env), command[0])
+            _command = [interpreter, *command[1:]]
+            return subprocess.run(_command, **kwargs, **self.run_opt)
+        else:
+            raise ValueError('invalid command: %s' % command)
diff --git a/codepack/plugin/manager.py b/codepack/plugin/manager.py
new file mode 100644
index 0000000..cf110ac
--- /dev/null
+++ b/codepack/plugin/manager.py
@@ -0,0 +1,8 @@
+import abc
+from typing import Union, Any
+
+
+class Manager(metaclass=abc.ABCMeta):
+    @abc.abstractmethod
+    def run(self, command: Union[str, list], **kwargs: Any) -> Any:
+        """run with given command"""
diff --git a/codepack/scheduler/scheduler.py b/codepack/plugin/scheduler.py
similarity index 50%
rename from codepack/scheduler/scheduler.py
rename to codepack/plugin/scheduler.py
index c855dea..b78ff6d 100644
--- a/codepack/scheduler/scheduler.py
+++ b/codepack/plugin/scheduler.py
@@ -1,20 +1,28 @@
-import abc
-from apscheduler.schedulers.background import BackgroundScheduler, BlockingScheduler
-from codepack import CodePack
+from codepack.codepack import CodePack
 from codepack.argpack import ArgPack
-from codepack.snapshot import CodePackSnapshot
-from codepack.employee import Supervisor
+from codepack.snapshot.codepack_snapshot import CodePackSnapshot
+from codepack.plugin.supervisor import Supervisor
+from apscheduler.schedulers.background import BackgroundScheduler, BlockingScheduler
 import requests
 import json
+from typing import Optional, Union, Callable, TypeVar, Any
+
+
+BaseJobStore = TypeVar('BaseJobStore', bound='apscheduler.jobstores.base.BaseJobStore')
+BaseTrigger = TypeVar('BaseTrigger', bound='apscheduler.triggers.base.BaseTrigger')
+Job = TypeVar('Job', bound='apscheduler.job.Job')
+Response = TypeVar('Response', bound='requests.models.Response')
 
 
-class Scheduler(metaclass=abc.ABCMeta):
+class Scheduler:
     supervisor = None
 
-    def __init__(self, callback=None, blocking=False, supervisor=None):
+    def __init__(self, callback: Optional[Callable] = None, jobstore: Optional[BaseJobStore] = None,
+                 blocking: bool = False, supervisor: Optional[Union[Supervisor, str]] = None) -> None:
         self.scheduler = None
         self.jobstores = dict()
-        self.jobstores['codepack'] = self.get_jobstore()
+        if jobstore:
+            self.jobstores['codepack'] = jobstore
         self.callback = None
         self.init_scheduler(blocking=blocking)
         self.register(callback)
@@ -24,54 +32,53 @@ def __init__(self, callback=None, blocking=False, supervisor=None):
             raise TypeError(type(supervisor))
 
     @classmethod
-    def init_supervisor(cls, supervisor):
+    def init_supervisor(cls, supervisor: Optional[Union[Supervisor, str]] = None):
         Scheduler.supervisor = supervisor
 
-    def register(self, callback):
+    def register(self, callback: Optional[Callable] = None):
         self.callback = callback
 
-    def start(self):
+    def start(self) -> None:
         try:
             self.scheduler.start()
         except KeyboardInterrupt:
             self.stop()
 
-    def init_scheduler(self, blocking=False, **kwargs):
+    def init_scheduler(self, blocking: bool = False, **kwargs: Any) -> None:
         if blocking:
             self.scheduler = BlockingScheduler(jobstores=self.jobstores, **kwargs)
         else:
             self.scheduler = BackgroundScheduler(jobstores=self.jobstores, **kwargs)
 
-    def is_running(self):
+    def is_running(self) -> bool:
         return self.scheduler.running
 
-    @abc.abstractmethod
-    def get_jobstore(self):
-        """get jobstore"""
-
-    def stop(self):
+    def stop(self) -> None:
         self.scheduler.shutdown()
 
-    def add_job(self, func, job_id, trigger, **kwargs):
+    def add_job(self, func: Callable, job_id: str, trigger: Union[BaseTrigger, str], **kwargs: Any) -> Job:
         return self.scheduler.add_job(func=func, id=job_id, trigger=trigger, jobstore='codepack', **kwargs)
 
-    def add_codepack(self, codepack, trigger, job_id=None, argpack=None, callback=None, **kwargs):
+    def add_codepack(self, codepack: CodePack, trigger: Union[BaseTrigger, str],
+                     job_id: Optional[str] = None, argpack: Optional[Union[ArgPack, dict]] = None,
+                     callback: Optional[Callable] = None, **kwargs: Any) -> Job:
         if job_id is None:
             job_id = codepack.id
         snapshot = codepack.to_snapshot(argpack=argpack)
-        ret = self.add_job(self._get_callback(callback), job_id=job_id, trigger=trigger, kwargs={'snapshot': snapshot.to_dict()}, **kwargs)
-        return ret
+        return self.add_job(self._get_callback(callback), job_id=job_id, trigger=trigger,
+                            kwargs={'snapshot': snapshot.to_dict()}, **kwargs)
 
-    def add_snapshot(self, snapshot, trigger, job_id=None, callback=None, **kwargs):
+    def add_snapshot(self, snapshot: CodePackSnapshot, trigger: Union[BaseTrigger, str], job_id: Optional[str] = None,
+                     callback: Optional[Callable] = None, **kwargs: Any) -> Job:
         if job_id is None:
             job_id = snapshot.id
-        ret = self.add_job(self._get_callback(callback), job_id=job_id, trigger=trigger, kwargs={'snapshot': snapshot.to_dict()}, **kwargs)
-        return ret
+        return self.add_job(self._get_callback(callback), job_id=job_id, trigger=trigger,
+                            kwargs={'snapshot': snapshot.to_dict()}, **kwargs)
 
-    def remove_job(self, job_id, **kwargs):
-        return self.scheduler.remove_job(job_id=job_id, **kwargs)
+    def remove_job(self, job_id: str, **kwargs: Any) -> None:
+        self.scheduler.remove_job(job_id=job_id, **kwargs)
 
-    def _get_callback(self, callback=None):
+    def _get_callback(self, callback: Optional[Callable] = None) -> Callable:
         if callback:
             _callback = callback
         elif self.callback:
@@ -83,11 +90,11 @@ def _get_callback(self, callback=None):
         return _callback
 
     @staticmethod
-    def _get_snapshot(snapshot):
-        if isinstance(snapshot, dict):
-            _snapshot = CodePackSnapshot.from_dict(snapshot)
-        elif isinstance(snapshot, CodePackSnapshot):
+    def _get_snapshot(snapshot: Union[CodePackSnapshot, dict, str]) -> CodePackSnapshot:
+        if isinstance(snapshot, CodePackSnapshot):
             _snapshot = snapshot
+        elif isinstance(snapshot, dict):
+            _snapshot = CodePackSnapshot.from_dict(snapshot)
         elif isinstance(snapshot, str):
             _snapshot = CodePackSnapshot.from_json(snapshot)
         else:
@@ -95,15 +102,15 @@ def _get_snapshot(snapshot):
         return _snapshot
 
     @staticmethod
-    def _get_codepack(snapshot):
+    def _get_codepack(snapshot: CodePackSnapshot) -> CodePack:
         return CodePack.from_snapshot(snapshot)
 
     @staticmethod
-    def _get_argpack(snapshot):
+    def _get_argpack(snapshot: Union[CodePackSnapshot, dict]) -> ArgPack:
         return ArgPack.from_dict(snapshot['argpack'])
 
     @staticmethod
-    def request_to_supervisor(snapshot):
+    def request_to_supervisor(snapshot: Union[CodePackSnapshot, dict]) -> Union[Response, str]:
         _snapshot = Scheduler._get_snapshot(snapshot)
         if isinstance(Scheduler.supervisor, str):
             return requests.post('%s/codepack/run/snapshot' % Scheduler.supervisor,
@@ -114,7 +121,7 @@ def request_to_supervisor(snapshot):
             return Scheduler.supervisor.run_codepack(codepack=codepack, argpack=argpack)
 
     @staticmethod
-    def run_snapshot(snapshot):
+    def run_snapshot(snapshot: Union[CodePackSnapshot, dict, str]) -> Any:
         _snapshot = Scheduler._get_snapshot(snapshot)
         codepack = Scheduler._get_codepack(_snapshot)
         argpack = Scheduler._get_argpack(_snapshot)
diff --git a/codepack/plugin/service.py b/codepack/plugin/service.py
new file mode 100644
index 0000000..eb98b72
--- /dev/null
+++ b/codepack/plugin/service.py
@@ -0,0 +1,13 @@
+import abc
+from typing import TypeVar
+
+
+Storage = TypeVar('Storage', bound='codepack.storage.storage.Storage')
+
+
+class Service(metaclass=abc.ABCMeta):
+    def __init__(self, storage: Storage) -> None:
+        self.storage = storage
+
+    def close(self) -> None:
+        self.storage.close()
diff --git a/codepack/plugin/snapshot_service.py b/codepack/plugin/snapshot_service.py
new file mode 100644
index 0000000..6f2aaa7
--- /dev/null
+++ b/codepack/plugin/snapshot_service.py
@@ -0,0 +1,39 @@
+from codepack.plugin.service import Service
+from codepack.snapshot.snapshot import Snapshot
+from codepack.snapshot.state import State
+from codepack.snapshot.snapshotable import Snapshotable
+from typing import Union, TypeVar, Optional, Any
+
+
+Storage = TypeVar('Storage', bound='codepack.storage.storage.Storage')
+Storable = TypeVar('Storable', bound='codepack.storage.storable.Storable')
+
+
+class SnapshotService(Service):
+    def __init__(self, storage: Storage) -> None:
+        super().__init__(storage=storage)
+        if self.storage.key != 'serial_number':
+            self.storage.key = 'serial_number'
+
+    def save(self, snapshot: Snapshot) -> None:
+        if self.storage.exist(key=snapshot.serial_number):
+            existing_snapshot = self.storage.load(key=snapshot.serial_number)
+            diff = existing_snapshot.diff(snapshot)
+            if 'state' in diff and isinstance(diff['state'], State):
+                diff['state'] = diff['state'].name
+            self.storage.update(key=snapshot.serial_number, values=diff)
+        else:
+            self.storage.save(item=snapshot)
+
+    def load(self, serial_number: Union[str, list], projection: Optional[list] = None)\
+            -> Optional[Union[Storable, dict, list]]:
+        return self.storage.load(key=serial_number, projection=projection, to_dict=True)
+
+    def remove(self, serial_number: Union[str, list]):
+        self.storage.remove(key=serial_number)
+
+    def search(self, key: str, value: Any, projection: Optional[list] = None) -> list:
+        return self.storage.search(key=key, value=value, projection=projection, to_dict=True)
+
+    def convert_to_snapshot(self, item: Snapshotable, *args: Any, **kwargs: Any) -> Snapshot:
+        return self.storage.item_type(item, *args, **kwargs)
diff --git a/codepack/plugin/storage_service.py b/codepack/plugin/storage_service.py
new file mode 100644
index 0000000..3ed09cf
--- /dev/null
+++ b/codepack/plugin/storage_service.py
@@ -0,0 +1,25 @@
+from codepack.plugin.service import Service
+from typing import Union, TypeVar, Optional
+
+
+Storage = TypeVar('Storage', bound='codepack.storage.storage.Storage')
+Storable = TypeVar('Storable', bound='codepack.storage.storable.Storable')
+
+
+class StorageService(Service):
+    def __init__(self, storage: Storage) -> None:
+        super().__init__(storage=storage)
+        if self.storage.key != 'id':
+            self.storage.key = 'id'
+
+    def save(self, item: Union[Storable, list], update: bool = False) -> None:
+        self.storage.save(item=item, update=update)
+
+    def load(self, id: Union[str, list]) -> Optional[Union[Storable, dict, list]]:
+        return self.storage.load(key=id)
+
+    def remove(self, id: Union[str, list]) -> None:
+        self.storage.remove(key=id)
+
+    def check(self, id: Union[str, list]) -> Union[bool, list]:
+        return self.storage.exist(key=id)
diff --git a/codepack/employee/supervisor.py b/codepack/plugin/supervisor.py
similarity index 56%
rename from codepack/employee/supervisor.py
rename to codepack/plugin/supervisor.py
index 8be98c0..e137a61 100644
--- a/codepack/employee/supervisor.py
+++ b/codepack/plugin/supervisor.py
@@ -1,28 +1,36 @@
-from codepack import Code, CodePack
-from codepack.config import Default
-from codepack.snapshot import CodeSnapshot
-from codepack.storage import KafkaStorage
+from codepack.code import Code
+from codepack.codepack import CodePack
+from codepack.config.default import Default
+from codepack.snapshot.code_snapshot import CodeSnapshot
+from codepack.plugin.employee import Employee
+from codepack.arg import Arg
+from codepack.argpack import ArgPack
+from typing import TypeVar, Optional, Union
 
 
-class Supervisor(KafkaStorage):
-    def __init__(self, producer=None, topic=None, snapshot_service=None, producer_config=None):
-        KafkaStorage.__init__(self, producer=producer, topic=topic, producer_config=producer_config)
+Messenger = TypeVar('Messenger', bound='codepack.storage.messenger.Messenger')
+SnapshotService = TypeVar('SnapshotService', bound='codepack.plugin.snapshot_service.SnapshotService')
+
+
+class Supervisor(Employee):
+    def __init__(self, messenger: Messenger, snapshot_service: Optional[SnapshotService] = None) -> None:
+        super().__init__(messenger=messenger)
         self.snapshot_service =\
-            snapshot_service if snapshot_service else Default.get_storage_instance('code_snapshot',
-                                                                                   'snapshot_service')
+            snapshot_service if snapshot_service else Default.get_service('code_snapshot', 'snapshot_service')
 
-    def run_code(self, code, args=None, kwargs=None):
+    def run_code(self, code: Code, args: Optional[tuple] = None, kwargs: Optional[Union[Arg, dict]] = None) -> str:
         if isinstance(code, Code):
             code.update_state('READY')
-            if kwargs and not isinstance(kwargs, dict):
+            if kwargs and isinstance(kwargs, Arg):
                 _kwargs = kwargs.to_dict()
             else:
                 _kwargs = kwargs
-            self.producer.produce(self.topic, value=code.to_snapshot(args=args, kwargs=_kwargs).to_dict())
+            self.messenger.send(item=code.to_snapshot(args=args, kwargs=_kwargs).to_dict())
         else:
             raise TypeError(type(code))
+        return code.serial_number
 
-    def run_codepack(self, codepack, argpack=None):
+    def run_codepack(self, codepack: CodePack, argpack: Optional[Union[ArgPack, dict]] = None) -> str:
         if isinstance(codepack, CodePack):
             codepack.save_snapshot(argpack=argpack)
             codepack.init_code_state(state='READY', argpack=argpack)
@@ -31,8 +39,9 @@ def run_codepack(self, codepack, argpack=None):
                 self.run_code(code=code, kwargs=_kwargs)
         else:
             raise TypeError(type(codepack))
+        return codepack.serial_number
 
-    def organize(self, serial_number=None):
+    def organize(self, serial_number: Optional[str] = None) -> None:
         for snapshot in self.snapshot_service.search(key='state', value='WAITING'):
             resolved = True
             dependent_serial_numbers = [snapshot['serial_number'] for snapshot in snapshot['dependency']]
diff --git a/codepack/plugin/worker.py b/codepack/plugin/worker.py
new file mode 100644
index 0000000..642d39a
--- /dev/null
+++ b/codepack/plugin/worker.py
@@ -0,0 +1,164 @@
+from codepack.code import Code
+from codepack.snapshot.code_snapshot import CodeSnapshot
+from codepack.plugin.supervisor import Supervisor
+from codepack.plugin.employee import Employee
+from codepack.storage.file_storage import FileStorage
+from codepack.config.default import Default
+from codepack.plugin.docker_manager import DockerManager
+from codepack.plugin.interpreter_manager import InterpreterManager
+from codepack.plugin.callback_service import CallbackService
+from codepack.callback.functions import inform_supervisor_of_termination
+from functools import partial
+import logging
+import os
+import sys
+from typing import TypeVar, Union, Optional, Callable
+
+
+Messenger = TypeVar('Messenger', bound='codepack.storage.messenger.Messenger')
+
+
+class Worker(Employee):
+    def __init__(self, messenger: Messenger, interval: Union[float, str] = 1,
+                 script_path: str = 'run_snapshot.py', callback: Optional[Callable] = None,
+                 supervisor: Optional[Union[Supervisor, str]] = None, background: Union[bool, str] = False,
+                 docker_manager: Optional[DockerManager] = None,
+                 interpreter_manager: Optional[InterpreterManager] = None,
+                 callback_service: Optional[CallbackService] = None,
+                 logger: Optional[Union[logging.Logger, str]] = None) -> None:
+        super().__init__(messenger=messenger)
+        self.interval = float(interval)
+        self.supervisor = supervisor
+        if isinstance(background, str):
+            if background.lower() == 'true':
+                self.background = True
+            elif background.lower() == 'false':
+                self.background = False
+            else:
+                raise ValueError("'background' should be either 'True' or 'False'")
+        elif isinstance(background, bool):
+            self.background = background
+        self.docker_manager = None
+        self.interpreter_manager = None
+        self.callback_service = None
+        self.script_path = script_path
+        self.script_dir = os.path.abspath(os.path.dirname(self.script_path))
+        self.script = os.path.basename(self.script_path)
+        self.callback = callback
+        if logger:
+            if isinstance(logger, logging.Logger):
+                self.logger = logger
+            elif isinstance(logger, str):
+                self.logger = Default.get_logger(logger)
+            else:
+                raise TypeError(logger)
+        else:
+            self.logger = None
+        if self.logger:
+            sys.stdout.write = partial(self.log, self.logger.info)
+        print('initializing worker...')
+        if self.supervisor and not self.callback:
+            if isinstance(self.supervisor, str) or isinstance(self.supervisor, Supervisor):
+                self.callback = partial(inform_supervisor_of_termination, supervisor=self.supervisor)
+            else:
+                self.close()
+                raise TypeError(type(self.supervisor))
+        self.init_docker_manager(docker_manager=docker_manager)
+        self.init_interpreter_manager(interpreter_manager=interpreter_manager)
+        self.init_callback_service(callback_service=callback_service)
+
+    @staticmethod
+    def log(function: Callable, message: str) -> None:
+        _message = message.strip()
+        if _message:
+            function(_message)
+
+    def init_docker_manager(self, docker_manager: Optional[DockerManager] = None) -> None:
+        if docker_manager is None:
+            self.docker_manager = Default.get_docker_manager()
+        elif isinstance(docker_manager, DockerManager):
+            self.docker_manager = docker_manager
+        else:
+            raise TypeError(type(docker_manager))
+
+    def init_interpreter_manager(self, interpreter_manager: Optional[InterpreterManager] = None) -> None:
+        if interpreter_manager is None:
+            self.interpreter_manager = Default.get_interpreter_manager()
+        elif isinstance(interpreter_manager, InterpreterManager):
+            self.interpreter_manager = interpreter_manager
+        else:
+            raise TypeError(type(interpreter_manager))
+
+    def init_callback_service(self, callback_service: Optional[CallbackService] = None) -> None:
+        if callback_service is None:
+            self.callback_service = Default.get_service('callback', 'callback_service')
+        elif isinstance(callback_service, CallbackService):
+            self.callback_service = callback_service
+        else:
+            raise TypeError(type(callback_service))
+
+    def start(self) -> None:
+        print('starting worker...')
+        self.messenger.receive(callback=self.work, background=self.background,
+                               interval=self.interval)
+
+    def stop(self) -> None:
+        print('stopping worker...')
+        self.close()
+
+    def work(self, buffer: dict) -> None:
+        for tp, msgs in buffer.items():
+            for msg in msgs:
+                snapshot = CodeSnapshot.from_dict(msg.value)
+                self.run_snapshot(snapshot=snapshot)
+
+    def run_snapshot(self, snapshot: CodeSnapshot) -> str:
+        snapshot_path = None
+        cb_id = None
+        code = Code.from_snapshot(snapshot)
+        code.register_callback(callback=self.callback)
+        try:
+            if code.image or code.env:
+                state = code.check_dependency()
+                cb_id = self.callback_service.push(self.callback)
+                if state == 'READY':
+                    filepath = '%s.json' % code.serial_number
+                    snapshot_path = os.path.join(self.script_dir, filepath)
+                    script_path = os.path.join(self.script_dir, self.script)
+                    snapshot.to_file(snapshot_path)
+                    if code.env:
+                        _command = ['python', script_path, snapshot_path, '-c', cb_id]
+                        if isinstance(self.callback_service.storage, FileStorage):
+                            _command.append('-p')
+                            _command.append(self.callback_service.storage.path)
+                        if self.logger:
+                            _command.append('-l')
+                            _command.append(self.logger.name)
+                        self.interpreter_manager.run(env=code.env, command=_command)
+                    else:  # if code.image:
+                        _command = ['python', self.script, filepath, '-c', cb_id]
+                        if isinstance(self.callback_service.storage, FileStorage):
+                            _command.append('-p')
+                            _command.append('.')
+                        if self.logger:
+                            _command.append('-l')
+                            _command.append(self.logger.name)
+                        ret = self.docker_manager.run(image=code.image, command=_command, path=self.script_dir)
+                        print(ret.decode('utf-8').strip())
+                else:
+                    code.update_state(state, args=snapshot.args, kwargs=snapshot.kwargs)
+            else:
+                code(*snapshot.args, **snapshot.kwargs)
+        except Exception as e:
+            if self.logger:
+                self.logger.error(e)
+            else:
+                print(e)
+            if code is not None:
+                code.update_state('ERROR', args=snapshot.args, kwargs=snapshot.kwargs, message=str(e))
+        finally:
+            if snapshot_path:
+                self.docker_manager.remove_file_if_exists(path=snapshot_path)
+            if cb_id and self.callback_service.exist(name=cb_id):
+                self.callback_service.remove(name=cb_id)
+            return snapshot['serial_number']
diff --git a/codepack/scheduler/__init__.py b/codepack/scheduler/__init__.py
deleted file mode 100644
index 9463a71..0000000
--- a/codepack/scheduler/__init__.py
+++ /dev/null
@@ -1,20 +0,0 @@
-from codepack.scheduler.scheduler import Scheduler
-from codepack.scheduler.mongo_scheduler import MongoScheduler
-from codepack.config import Config
-import os
-
-
-def get_default_scheduler(config_path=None):
-    config = Config(config_path=config_path)
-    storage_config = config.get_storage_config(section='scheduler')
-    source = storage_config.pop('source', None)
-    supervisor = storage_config.pop('supervisor', None)
-    if 'CODEPACK_SCHEDULER_SUPERVISOR' in os.environ:
-        supervisor = os.environ['CODEPACK_SCHEDULER_SUPERVISOR']
-    # cls = SchedulerAlias[source].value
-    scheduler = MongoScheduler(**storage_config)
-    if supervisor:
-        if 'CODEPACK_SCHEDULER_SUPERVISOR' not in os.environ:
-            os.environ['CODEPACK_SCHEDULER_SUPERVISOR'] = supervisor
-        scheduler.register(scheduler.request_to_supervisor)
-    return scheduler
diff --git a/codepack/scheduler/jobstore/__init__.py b/codepack/scheduler/jobstore/__init__.py
deleted file mode 100644
index 3b72567..0000000
--- a/codepack/scheduler/jobstore/__init__.py
+++ /dev/null
@@ -1,1 +0,0 @@
-from codepack.scheduler.jobstore.mongo_jobstore import MongoJobStore
diff --git a/codepack/scheduler/mongo_scheduler.py b/codepack/scheduler/mongo_scheduler.py
deleted file mode 100644
index 0775e85..0000000
--- a/codepack/scheduler/mongo_scheduler.py
+++ /dev/null
@@ -1,12 +0,0 @@
-from codepack.scheduler import Scheduler
-from codepack.scheduler.jobstore import MongoJobStore
-from codepack.storage import MongoStorage
-
-
-class MongoScheduler(MongoStorage, Scheduler):
-    def __init__(self, mongodb=None, db=None, collection=None, blocking=False, callback=None, supervisor=None, *args, **kwargs):
-        MongoStorage.__init__(self, mongodb=mongodb, db=db, collection=collection, *args, **kwargs)
-        Scheduler.__init__(self, blocking=blocking, callback=callback, supervisor=supervisor)
-
-    def get_jobstore(self):
-        return MongoJobStore(db=self.db, collection=self.collection, client=self.mongodb.session)
diff --git a/codepack/service/__init__.py b/codepack/service/__init__.py
deleted file mode 100644
index 60e64c1..0000000
--- a/codepack/service/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-from codepack.service.delivery_service import MemoryDeliveryService, FileDeliveryService, MongoDeliveryService
-from codepack.service.storage_service import MemoryStorageService, FileStorageService, MongoStorageService
-from codepack.service.snapshot_service import MemorySnapshotService, FileSnapshotService, MongoSnapshotService
-from codepack.service.service import Service
diff --git a/codepack/service/delivery_service/__init__.py b/codepack/service/delivery_service/__init__.py
deleted file mode 100644
index e1dd3e7..0000000
--- a/codepack/service/delivery_service/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-from codepack.service.delivery_service.delivery_service import DeliveryService
-from codepack.service.delivery_service.memory_delivery_service import MemoryDeliveryService
-from codepack.service.delivery_service.file_delivery_service import FileDeliveryService
-from codepack.service.delivery_service.mongo_delivery_service import MongoDeliveryService
diff --git a/codepack/service/delivery_service/delivery_service.py b/codepack/service/delivery_service/delivery_service.py
deleted file mode 100644
index 7382ea8..0000000
--- a/codepack/service/delivery_service/delivery_service.py
+++ /dev/null
@@ -1,23 +0,0 @@
-import abc
-from codepack.service.service import Service
-
-
-class DeliveryService(Service, metaclass=abc.ABCMeta):
-    def __init__(self):
-        super().__init__()
-
-    @abc.abstractmethod
-    def send(self, id, serial_number, item=None, timestamp=None):
-        """send item"""
-
-    @abc.abstractmethod
-    def receive(self, serial_number):
-        """receive item"""
-
-    @abc.abstractmethod
-    def cancel(self, serial_number):
-        """cancel delivery"""
-
-    @abc.abstractmethod
-    def check(self, serial_number):
-        """check arrival of delivery"""
diff --git a/codepack/service/delivery_service/file_delivery_service.py b/codepack/service/delivery_service/file_delivery_service.py
deleted file mode 100644
index 4d74f1c..0000000
--- a/codepack/service/delivery_service/file_delivery_service.py
+++ /dev/null
@@ -1,37 +0,0 @@
-import os
-from codepack.storage import FileStorage
-from codepack.service.delivery_service import DeliveryService
-from collections.abc import Iterable
-
-
-class FileDeliveryService(DeliveryService, FileStorage):
-    def __init__(self, item_type=None, path='./'):
-        FileStorage.__init__(self, item_type=item_type, path=path)
-
-    def send(self, id, serial_number, item=None, timestamp=None):
-        self.item_type(id=id, serial_number=serial_number, item=item, timestamp=timestamp)\
-            .to_file(path=self.item_type.get_path(serial_number=serial_number, path=self.path))
-
-    def receive(self, serial_number):
-        return self.item_type.from_file(path=self.item_type.get_path(serial_number=serial_number, path=self.path)).receive()
-
-    def check(self, serial_number):
-        if isinstance(serial_number, str):
-            d = None
-            try:
-                d = self.item_type.from_file(path=self.item_type.get_path(serial_number=serial_number, path=self.path)).to_dict()
-                d.pop('item', None)
-            finally:
-                return d
-        elif isinstance(serial_number, Iterable):
-            ret = list()
-            for i in serial_number:
-                tmp = self.check(i)
-                if tmp:
-                    ret.append(tmp)
-            return ret
-        else:
-            raise TypeError(type(serial_number))  # pragma: no cover
-
-    def cancel(self, serial_number):
-        os.remove(self.item_type.get_path(serial_number=serial_number, path=self.path))
diff --git a/codepack/service/delivery_service/memory_delivery_service.py b/codepack/service/delivery_service/memory_delivery_service.py
deleted file mode 100644
index a771ae6..0000000
--- a/codepack/service/delivery_service/memory_delivery_service.py
+++ /dev/null
@@ -1,35 +0,0 @@
-from codepack.storage import MemoryStorage
-from codepack.service.delivery_service import DeliveryService
-from collections.abc import Iterable
-
-
-class MemoryDeliveryService(DeliveryService, MemoryStorage):
-    def __init__(self, item_type=None):
-        MemoryStorage.__init__(self, item_type=item_type)
-
-    def send(self, id, serial_number, item=None, timestamp=None):
-        self.memory[serial_number] = self.item_type(id=id, serial_number=serial_number, item=item, timestamp=timestamp)
-
-    def receive(self, serial_number):
-        return self.memory[serial_number].receive()
-
-    def check(self, serial_number):
-        if isinstance(serial_number, str):
-            if serial_number in self.memory:
-                d = self.memory[serial_number].to_dict()
-                d.pop('item', None)
-                return d
-            else:
-                return None
-        elif isinstance(serial_number, Iterable):
-            ret = list()
-            for i in serial_number:
-                tmp = self.check(i)
-                if tmp:
-                    ret.append(tmp)
-            return ret
-        else:
-            raise TypeError(type(serial_number))  # pragma: no cover
-
-    def cancel(self, serial_number):
-        self.memory.pop(serial_number, None)
diff --git a/codepack/service/delivery_service/mongo_delivery_service.py b/codepack/service/delivery_service/mongo_delivery_service.py
deleted file mode 100644
index 6d0844f..0000000
--- a/codepack/service/delivery_service/mongo_delivery_service.py
+++ /dev/null
@@ -1,29 +0,0 @@
-from codepack.storage import MongoStorage
-from codepack.service.delivery_service import DeliveryService
-from collections.abc import Iterable
-import json
-
-
-class MongoDeliveryService(DeliveryService, MongoStorage):
-    def __init__(self, item_type=None, db=None, collection=None, mongodb=None, *args, **kwargs):
-        MongoStorage.__init__(self, item_type=item_type, db=db, collection=collection, mongodb=mongodb, *args, **kwargs)
-
-    def send(self, id, serial_number, item=None, timestamp=None):
-        d = self.item_type(id=id, serial_number=serial_number, item=item, timestamp=timestamp).to_dict()
-        d.pop('_id', None)
-        self.mongodb[self.db][self.collection].update_one({'_id': serial_number}, {'$set': d}, upsert=True)
-
-    def receive(self, serial_number):
-        ret = self.mongodb[self.db][self.collection].find_one({'_id': serial_number})
-        return json.loads(ret['item'])
-
-    def check(self, serial_number):
-        if isinstance(serial_number, str):
-            return self.mongodb[self.db][self.collection].find_one({'_id': serial_number}, projection={'item': 0})
-        elif isinstance(serial_number, Iterable):
-            return list(self.mongodb[self.db][self.collection].find({'_id': {'$in': serial_number}}, projection={'item': 0}))
-        else:
-            raise TypeError(type(serial_number))  # pragma: no cover
-
-    def cancel(self, serial_number):
-        self.mongodb[self.db][self.collection].delete_one({'_id': serial_number})
diff --git a/codepack/service/service.py b/codepack/service/service.py
deleted file mode 100644
index 8b26dc4..0000000
--- a/codepack/service/service.py
+++ /dev/null
@@ -1,5 +0,0 @@
-import abc
-
-
-class Service(metaclass=abc.ABCMeta):
-    pass
diff --git a/codepack/service/snapshot_service/__init__.py b/codepack/service/snapshot_service/__init__.py
deleted file mode 100644
index 93d23dc..0000000
--- a/codepack/service/snapshot_service/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-from codepack.service.snapshot_service.snapshot_service import SnapshotService
-from codepack.service.snapshot_service.memory_snapshot_service import MemorySnapshotService
-from codepack.service.snapshot_service.file_snapshot_service import FileSnapshotService
-from codepack.service.snapshot_service.mongo_snapshot_service import MongoSnapshotService
diff --git a/codepack/service/snapshot_service/file_snapshot_service.py b/codepack/service/snapshot_service/file_snapshot_service.py
deleted file mode 100644
index 4821d35..0000000
--- a/codepack/service/snapshot_service/file_snapshot_service.py
+++ /dev/null
@@ -1,60 +0,0 @@
-import os
-from codepack.storage import FileStorage
-from codepack.service.snapshot_service import SnapshotService
-from collections.abc import Iterable
-from glob import glob
-
-
-class FileSnapshotService(SnapshotService, FileStorage):
-    def __init__(self, item_type=None, path='./'):
-        FileStorage.__init__(self, item_type=item_type, path=path)
-
-    def save(self, snapshot):
-        if isinstance(snapshot, self.item_type):
-            d = self.load(snapshot.serial_number)
-            if d:
-                existing_snapshot = self.item_type.from_dict(d)
-                for key, value in existing_snapshot.diff(snapshot).items():
-                    existing_snapshot[key] = value
-                    existing_snapshot.to_file(self.item_type.get_path(serial_number=existing_snapshot.serial_number, path=self.path))
-            else:
-                snapshot.to_file(self.item_type.get_path(serial_number=snapshot.serial_number, path=self.path))
-        else:
-            raise TypeError(type(snapshot))
-
-    def load(self, serial_number, projection=None):
-        if isinstance(serial_number, str):
-            ret = None
-            try:
-                d = self.item_type.from_file(self.item_type.get_path(serial_number=serial_number, path=self.path)).to_dict()
-                if projection:
-                    ret = {k: d[k] for k in set(projection).union({'serial_number'})}
-                else:
-                    ret = d
-            finally:
-                return ret
-        elif isinstance(serial_number, Iterable):
-            ret = list()
-            for s in serial_number:
-                tmp = self.load(serial_number=s, projection=projection)
-                if tmp:
-                    ret.append(tmp)
-            return ret
-        else:
-            raise TypeError(type(serial_number))  # pragma: no cover
-
-    def remove(self, serial_number):
-        os.remove(self.item_type.get_path(serial_number=serial_number, path=self.path))
-
-    def search(self, key, value, projection=None):
-        ret = list()
-        for filename in glob(self.path + '*.json'):
-            snapshot = self.item_type.from_file(filename)
-            if snapshot[key] != value:
-                continue
-            d = snapshot.to_dict()
-            if projection:
-                ret.append({k: d[k] for k in set(projection).union({'serial_number'})})
-            else:
-                ret.append(d)
-        return ret
diff --git a/codepack/service/snapshot_service/memory_snapshot_service.py b/codepack/service/snapshot_service/memory_snapshot_service.py
deleted file mode 100644
index 89c1b13..0000000
--- a/codepack/service/snapshot_service/memory_snapshot_service.py
+++ /dev/null
@@ -1,54 +0,0 @@
-from codepack.storage import MemoryStorage
-from codepack.service.snapshot_service import SnapshotService
-from collections.abc import Iterable
-
-
-class MemorySnapshotService(SnapshotService, MemoryStorage):
-    def __init__(self, item_type=None):
-        MemoryStorage.__init__(self, item_type=item_type)
-
-    def save(self, snapshot):
-        if isinstance(snapshot, self.item_type):
-            d = self.load(snapshot.serial_number)
-            if d:
-                for key, value in self.item_type.from_dict(d).diff(snapshot).items():
-                    self.memory[snapshot.serial_number][key] = value
-            else:
-                self.memory[snapshot.serial_number] = snapshot
-        else:
-            raise TypeError(type(snapshot))
-
-    def load(self, serial_number, projection=None):
-        if isinstance(serial_number, str):
-            if serial_number in self.memory:
-                d = self.memory[serial_number].to_dict()
-                if projection:
-                    return {k: d[k] for k in set(projection).union({'serial_number'})}
-                else:
-                    return d
-            else:
-                return None
-        elif isinstance(serial_number, Iterable):
-            ret = list()
-            for s in serial_number:
-                tmp = self.load(serial_number=s, projection=projection)
-                if tmp:
-                    ret.append(tmp)
-            return ret
-        else:
-            raise TypeError(type(serial_number))  # pragma: no cover
-
-    def remove(self, serial_number):
-        self.memory.pop(serial_number, None)
-
-    def search(self, key, value, projection=None):
-        ret = list()
-        for snapshot in self.memory.values():
-            if snapshot[key] != value:
-                continue
-            d = snapshot.to_dict()
-            if projection:
-                ret.append({k: d[k] for k in set(projection).union({'serial_number'})})
-            else:
-                ret.append(d)
-        return ret
diff --git a/codepack/service/snapshot_service/mongo_snapshot_service.py b/codepack/service/snapshot_service/mongo_snapshot_service.py
deleted file mode 100644
index e5054da..0000000
--- a/codepack/service/snapshot_service/mongo_snapshot_service.py
+++ /dev/null
@@ -1,52 +0,0 @@
-from codepack.storage import MongoStorage
-from codepack.service.snapshot_service import SnapshotService
-from collections.abc import Iterable
-from codepack.snapshot import State
-
-
-class MongoSnapshotService(SnapshotService, MongoStorage):
-    def __init__(self, item_type=None, mongodb=None, db=None, collection=None, *args, **kwargs):
-        MongoStorage.__init__(self, item_type=item_type, mongodb=mongodb, db=db, collection=collection, *args, **kwargs)
-
-    def save(self, snapshot):
-        if isinstance(snapshot, self.item_type):
-            d = self.load(snapshot.serial_number)
-            if d:
-                existing_snapshot = self.item_type.from_dict(d)
-                diff = existing_snapshot.diff(snapshot)
-                for key, value in diff.items():
-                    existing_snapshot[key] = value
-                if 'state' in diff and isinstance(diff['state'], State):
-                    diff['state'] = diff['state'].name
-                self.mongodb[self.db][self.collection].update_one({'_id': existing_snapshot.serial_number},
-                                                                  {'$set': diff}, upsert=True)
-            else:
-                snapshot.to_db(mongodb=self.mongodb, db=self.db, collection=self.collection)
-        else:
-            raise TypeError(type(snapshot))
-
-    def load(self, serial_number, projection=None):
-        if projection:
-            _projection = {k: True for k in projection}
-            _projection['serial_number'] = True
-            _projection['_id'] = False
-        else:
-            _projection = projection
-        if isinstance(serial_number, str):
-            return self.mongodb[self.db][self.collection].find_one({'_id': serial_number}, projection=_projection)
-        elif isinstance(serial_number, Iterable):
-            return list(self.mongodb[self.db][self.collection].find({'_id': {'$in': serial_number}}, projection=_projection))
-        else:
-            raise TypeError(type(serial_number))  # pragma: no cover
-
-    def remove(self, serial_number):
-        self.mongodb[self.db][self.collection].delete_one({'_id': serial_number})
-
-    def search(self, key, value, projection=None):
-        if projection:
-            _projection = {k: True for k in projection}
-            _projection['serial_number'] = True
-            _projection['_id'] = False
-        else:
-            _projection = projection
-        return list(self.mongodb[self.db][self.collection].find({key: value}, projection=_projection))
diff --git a/codepack/service/snapshot_service/snapshot_service.py b/codepack/service/snapshot_service/snapshot_service.py
deleted file mode 100644
index 548bd16..0000000
--- a/codepack/service/snapshot_service/snapshot_service.py
+++ /dev/null
@@ -1,23 +0,0 @@
-import abc
-from codepack.service.service import Service
-
-
-class SnapshotService(Service, metaclass=abc.ABCMeta):
-    def __init__(self):
-        super().__init__()
-
-    @abc.abstractmethod
-    def save(self, snapshot):
-        """save snapshot"""
-
-    @abc.abstractmethod
-    def load(self, serial_number, projection=None):
-        """load snapshot"""
-
-    @abc.abstractmethod
-    def remove(self, serial_number):
-        """remove snapshot"""
-
-    @abc.abstractmethod
-    def search(self, key, value, projection=None):
-        """search by key and value"""
diff --git a/codepack/service/storage_service/__init__.py b/codepack/service/storage_service/__init__.py
deleted file mode 100644
index 6076b6c..0000000
--- a/codepack/service/storage_service/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-from codepack.service.storage_service.storage_service import StorageService
-from codepack.service.storage_service.memory_storage_service import MemoryStorageService
-from codepack.service.storage_service.file_storage_service import FileStorageService
-from codepack.service.storage_service.mongo_storage_service import MongoStorageService
diff --git a/codepack/service/storage_service/file_storage_service.py b/codepack/service/storage_service/file_storage_service.py
deleted file mode 100644
index 18478ed..0000000
--- a/codepack/service/storage_service/file_storage_service.py
+++ /dev/null
@@ -1,43 +0,0 @@
-import os
-from codepack.storage import FileStorage
-from codepack.service.storage_service import StorageService
-from collections.abc import Iterable
-
-
-class FileStorageService(StorageService, FileStorage):
-    def __init__(self, item_type=None, path='./'):
-        FileStorage.__init__(self, item_type=item_type, path=path)
-
-    def save(self, item, update=False):
-        if isinstance(item, self.item_type):
-            if update:
-                item.to_file(self.item_type.get_path(serial_number=item.id, path=self.path))
-            elif self.check(item.id):
-                raise ValueError('%s already exists' % item.id)
-            else:
-                item.to_file(self.item_type.get_path(serial_number=item.id, path=self.path))
-        else:
-            raise TypeError(type(item))
-
-    def load(self, id):
-        return self.item_type.from_file(path=self.item_type.get_path(serial_number=id, path=self.path))
-
-    def remove(self, id):
-        os.remove(path=self.item_type.get_path(serial_number=id, path=self.path))
-
-    def check(self, id):
-        if isinstance(id, str):
-            ret = None
-            try:
-                ret = self.item_type.from_file(path=self.item_type.get_path(serial_number=id, path=self.path)).id
-            finally:
-                return ret
-        elif isinstance(id, Iterable):
-            ret = list()
-            for i in id:
-                tmp = self.check(i)
-                if tmp:
-                    ret.append(tmp)
-            return ret
-        else:
-            raise TypeError(type(id))  # pragma: no cover
diff --git a/codepack/service/storage_service/memory_storage_service.py b/codepack/service/storage_service/memory_storage_service.py
deleted file mode 100644
index 045a75b..0000000
--- a/codepack/service/storage_service/memory_storage_service.py
+++ /dev/null
@@ -1,41 +0,0 @@
-from codepack.storage import MemoryStorage
-from codepack.service.storage_service import StorageService
-from collections.abc import Iterable
-
-
-class MemoryStorageService(StorageService, MemoryStorage):
-    def __init__(self, item_type=None):
-        MemoryStorage.__init__(self, item_type=item_type)
-
-    def save(self, item, update=False):
-        if isinstance(item, self.item_type):
-            if update:
-                self.memory[item.id] = item
-            elif self.check(item.id):
-                raise ValueError('%s already exists' % item.id)
-            else:
-                self.memory[item.id] = item
-        else:
-            raise TypeError(type(item))
-
-    def load(self, id):
-        return self.memory[id]
-
-    def remove(self, id):
-        self.memory.pop(id, None)
-
-    def check(self, id):
-        if isinstance(id, str):
-            if id in self.memory:
-                return self.memory[id].id
-            else:
-                return None
-        elif isinstance(id, Iterable):
-            ret = list()
-            for i in id:
-                tmp = self.check(i)
-                if tmp:
-                    ret.append(tmp)
-            return ret
-        else:
-            raise TypeError(type(id))  # pragma: no cover
diff --git a/codepack/service/storage_service/mongo_storage_service.py b/codepack/service/storage_service/mongo_storage_service.py
deleted file mode 100644
index 863a46a..0000000
--- a/codepack/service/storage_service/mongo_storage_service.py
+++ /dev/null
@@ -1,42 +0,0 @@
-from codepack.storage import MongoStorage
-from codepack.service.storage_service import StorageService
-from collections.abc import Iterable
-
-
-class MongoStorageService(StorageService, MongoStorage):
-    def __init__(self, item_type=None, mongodb=None, db=None, collection=None, *args, **kwargs):
-        MongoStorage.__init__(self, item_type=item_type, mongodb=mongodb, db=db, collection=collection, *args, **kwargs)
-
-    def save(self, item, update=False):
-        if isinstance(item, self.item_type):
-            if update:
-                d = item.to_dict()
-                _id = d.pop('_id')
-                self.mongodb[self.db][self.collection].update_one({'_id': _id}, {'$set': d}, upsert=True)
-            elif self.check(item.id):
-                raise ValueError('%s already exists' % item.id)
-            else:
-                item.to_db(mongodb=self.mongodb, db=self.db, collection=self.collection)
-        else:
-            raise TypeError(type(item))
-
-    def load(self, id):
-        return self.item_type.from_db(id=id, mongodb=self.mongodb, db=self.db, collection=self.collection)
-
-    def remove(self, id):
-        self.mongodb[self.db][self.collection].delete_one({'_id': id})
-
-    def check(self, id):
-        if isinstance(id, str):
-            ret = None
-            item = self.item_type.from_db(id=id, mongodb=self.mongodb, db=self.db, collection=self.collection)
-            if item:
-                ret = item.id
-            return ret
-        elif isinstance(id, Iterable):
-            ret = list()
-            for item in self.mongodb[self.db][self.collection].find({'_id': {'$in': id}}, projection={}):
-                ret.append(item['_id'])
-            return ret
-        else:
-            raise TypeError(type(id))  # pragma: no cover
diff --git a/codepack/service/storage_service/storage_service.py b/codepack/service/storage_service/storage_service.py
deleted file mode 100644
index a75f5ce..0000000
--- a/codepack/service/storage_service/storage_service.py
+++ /dev/null
@@ -1,20 +0,0 @@
-import abc
-from codepack.service.service import Service
-
-
-class StorageService(Service, metaclass=abc.ABCMeta):
-    @abc.abstractmethod
-    def save(self, item, update=False):
-        """save item"""
-
-    @abc.abstractmethod
-    def load(self, id):
-        """load item with given id"""
-
-    @abc.abstractmethod
-    def remove(self, id):
-        """remove item with given id"""
-
-    @abc.abstractmethod
-    def check(self, id):
-        """check if item with given id exists"""
diff --git a/codepack/snapshot/__init__.py b/codepack/snapshot/__init__.py
index 6e8488c..e69de29 100644
--- a/codepack/snapshot/__init__.py
+++ b/codepack/snapshot/__init__.py
@@ -1,5 +0,0 @@
-from codepack.snapshot.snapshot import Snapshot
-from codepack.snapshot.snapshotable import Snapshotable
-from codepack.snapshot.code_snapshot import CodeSnapshot
-from codepack.snapshot.codepack_snapshot import CodePackSnapshot
-from codepack.snapshot.state import State
diff --git a/codepack/snapshot/code_snapshot.py b/codepack/snapshot/code_snapshot.py
index 9d3d0a7..ba1eeae 100644
--- a/codepack/snapshot/code_snapshot.py
+++ b/codepack/snapshot/code_snapshot.py
@@ -1,31 +1,43 @@
-from codepack.snapshot import Snapshot
-from codepack.dependency import Dependency
+from codepack.snapshot.snapshot import Snapshot
+from codepack.dependency.dependency import Dependency
+from typing import TypeVar, Optional, Union
+
+
+Code = TypeVar('Code', bound='codepack.code.Code')
 
 
 class CodeSnapshot(Snapshot):
-    def __init__(self, code=None, args=None, kwargs=None, timestamp=None):
+    def __init__(self, code: Optional[Code] = None, args: Optional[tuple] = None, kwargs: Optional[dict] = None,
+                 timestamp: Optional[float] = None, message: str = '') -> None:
         if code:
             _id = code.id
             _serial_number = code.serial_number
             _state = code.get_state()
             _source = code.source
             _dependency = code.dependency
+            _env = code.env
+            _image = code.image
+            _owner = code.owner
         else:
             _id = None
             _serial_number = None
             _state = None
             _source = None
             _dependency = None
-        super().__init__(id=_id, serial_number=_serial_number, state=_state, timestamp=timestamp)
+            _env = None
+            _image = None
+            _owner = None
+        super().__init__(id=_id, serial_number=_serial_number, state=_state, timestamp=timestamp,
+                         env=_env, image=_image, owner=_owner, message=message)
         self.__setitem__('source', _source)
         self.set_args(args=args, kwargs=kwargs)
         self.set_dependency(dependency=_dependency)
 
-    def set_args(self, args=None, kwargs=None):
+    def set_args(self, args: Optional[tuple] = None, kwargs: Optional[dict] = None) -> None:
         self.__setitem__('args', list(args) if args else list())
         self.__setitem__('kwargs', kwargs if kwargs else dict())
 
-    def set_dependency(self, dependency):
+    def set_dependency(self, dependency: Union[Dependency, dict]) -> None:
         self.__setitem__('dependency', list())
         if dependency:
             for d in dependency.values():
@@ -38,7 +50,7 @@ def set_dependency(self, dependency):
             self.__getitem__('dependency').sort(key=lambda x: x['serial_number'])
 
     @classmethod
-    def from_dict(cls, d):
+    def from_dict(cls, d: dict) -> 'CodeSnapshot':
         ret = cls()
         for k, v in d.items():
             if k != '_id':
diff --git a/codepack/snapshot/codepack_snapshot.py b/codepack/snapshot/codepack_snapshot.py
index 8c29d87..9c5487a 100644
--- a/codepack/snapshot/codepack_snapshot.py
+++ b/codepack/snapshot/codepack_snapshot.py
@@ -1,10 +1,15 @@
-from codepack.snapshot import Snapshot
+from codepack.snapshot.snapshot import Snapshot
 from codepack.argpack import ArgPack
 from copy import deepcopy
+from typing import TypeVar, Optional, Union
+
+
+CodePack = TypeVar('CodePack', bound='codepack.codepack.CodePack')
 
 
 class CodePackSnapshot(Snapshot):
-    def __init__(self, codepack=None, argpack=None, timestamp=None):
+    def __init__(self, codepack: Optional[CodePack] = None, argpack: Optional[Union[ArgPack, dict]] = None,
+                 timestamp: Optional[float] = None) -> None:
         if codepack:
             _id = codepack.id
             _serial_number = codepack.serial_number
@@ -13,6 +18,7 @@ def __init__(self, codepack=None, argpack=None, timestamp=None):
             _source = codepack.get_source()
             _structure = codepack.get_structure()
             _subscribe = codepack.subscribe
+            _owner = codepack.owner
         else:
             _id = None
             _serial_number = None
@@ -21,14 +27,15 @@ def __init__(self, codepack=None, argpack=None, timestamp=None):
             _source = None
             _structure = None
             _subscribe = None
-        super().__init__(id=_id, serial_number=_serial_number, state=_state, timestamp=timestamp)
+            _owner = None
+        super().__init__(id=_id, serial_number=_serial_number, state=_state, timestamp=timestamp, owner=_owner)
         self.__setitem__('codes', _codes)
         self.__setitem__('source', _source)
         self.__setitem__('structure', _structure)
         self.__setitem__('subscribe', _subscribe)
         self.set_argpack(argpack=argpack)
 
-    def set_argpack(self, argpack=None):
+    def set_argpack(self, argpack: Optional[Union[ArgPack, dict]] = None) -> None:
         if isinstance(argpack, ArgPack):
             tmp = argpack.to_dict()
         elif isinstance(argpack, dict):
@@ -42,7 +49,7 @@ def set_argpack(self, argpack=None):
         self.__setitem__('argpack', tmp)
 
     @classmethod
-    def from_dict(cls, d):
+    def from_dict(cls, d: dict) -> 'CodePackSnapshot':
         ret = cls()
         for k, v in d.items():
             if k != '_id':
diff --git a/codepack/snapshot/snapshot.py b/codepack/snapshot/snapshot.py
index 605930d..fcff524 100644
--- a/codepack/snapshot/snapshot.py
+++ b/codepack/snapshot/snapshot.py
@@ -1,11 +1,13 @@
-from codepack.storage import Storable
-from datetime import datetime, timezone
+from codepack.storage.storable import Storable
 from codepack.snapshot.state import State
+from datetime import datetime, timezone
 from copy import deepcopy
+from typing import Optional, Any, Union, Iterator, KeysView, ValuesView, ItemsView
 
 
 class Snapshot(Storable):
-    def __init__(self, id, serial_number, state=None, timestamp=None, **kwargs):
+    def __init__(self, id: str, serial_number: str, state: Optional[Union[State, str]] = None,
+                 timestamp: Optional[float] = None, **kwargs: Any) -> None:
         super().__init__(id=id, serial_number=serial_number)
         self.attr = dict()
         self.__setitem__('id', self.id)
@@ -15,20 +17,20 @@ def __init__(self, id, serial_number, state=None, timestamp=None, **kwargs):
         for k, v in kwargs.items():
             self.__setitem__(k, v)
 
-    def __setitem__(self, key, value):
+    def __setitem__(self, key: str, value: Any) -> None:
         if key in ['id', 'serial_number']:
             self.__setattr__(key, value)
         if key == 'state':
             value = State.get(value)
         self.attr[key] = value
 
-    def __getitem__(self, item):
+    def __getitem__(self, item: str) -> Any:
         return self.attr[item]
 
-    def __getattr__(self, item):
+    def __getattr__(self, item: str) -> Any:
         return self.__getitem__(item)
 
-    def diff(self, snapshot):
+    def diff(self, snapshot: Union['Snapshot', dict]) -> dict:
         ret = dict()
         if isinstance(snapshot, self.__class__):
             for k, v in snapshot.items():
@@ -42,28 +44,29 @@ def diff(self, snapshot):
             raise TypeError(type(snapshot))  # pragma: no cover
         return ret
 
-    def to_dict(self):
+    def to_dict(self) -> dict:
         ret = dict()
         for k, v in self.attr.items():
             ret[k] = v
-        ret['state'] = ret['state'].name
+        if isinstance(ret['state'], State):
+            ret['state'] = ret['state'].name
         ret['_id'] = ret['serial_number']
         return ret
 
     @classmethod
-    def from_dict(cls, d):
+    def from_dict(cls, d: dict) -> 'Snapshot':
         attr = deepcopy(d)
         attr.pop('_id', None)
         return cls(**attr)
 
-    def __iter__(self):
+    def __iter__(self) -> Iterator:
         return self.attr.__iter__()
 
-    def items(self):
+    def items(self) -> ItemsView:
         return self.attr.items()
 
-    def keys(self):
-        return self.atttr.keys()
+    def keys(self) -> KeysView:
+        return self.attr.keys()
 
-    def values(self):
+    def values(self) -> ValuesView:
         return self.attr.values()
diff --git a/codepack/snapshot/snapshotable.py b/codepack/snapshot/snapshotable.py
index 4f02499..0c4d42f 100644
--- a/codepack/snapshot/snapshotable.py
+++ b/codepack/snapshot/snapshotable.py
@@ -1,12 +1,16 @@
 import abc
+from typing import Any, TypeVar
+
+
+Snapshot = TypeVar('Snapshot', bound='codepack.snapshot.snapshot.Snapshot')
 
 
 class Snapshotable(metaclass=abc.ABCMeta):
     @abc.abstractmethod
-    def to_snapshot(self, *args, **kwargs):
+    def to_snapshot(self, *args: Any, **kwargs: Any) -> Snapshot:
         """convert to snapshot"""
 
     @classmethod
     @abc.abstractmethod
-    def from_snapshot(cls, snapshot):
+    def from_snapshot(cls, snapshot: Snapshot) -> 'Snapshotable':
         """create instance from snapshot"""
diff --git a/codepack/snapshot/state.py b/codepack/snapshot/state.py
index 5f3640c..5d1a757 100644
--- a/codepack/snapshot/state.py
+++ b/codepack/snapshot/state.py
@@ -1,4 +1,5 @@
 from enum import Enum
+from typing import Union
 
 
 class State(Enum):
@@ -10,13 +11,13 @@ class State(Enum):
     TERMINATED = 5
     ERROR = 6
 
-    def __str__(self):
+    def __str__(self) -> str:
         return self.name  # pragma: no cover
 
-    def __repr__(self):
+    def __repr__(self) -> str:
         return self.__str__()  # pragma: no cover
 
-    def __eq__(self, other):
+    def __eq__(self, other: Union['State', str, int]) -> bool:
         if isinstance(other, type(self)):
             return self.name == other.name
         elif isinstance(other, str):
@@ -27,7 +28,7 @@ def __eq__(self, other):
             return False  # pragma: no cover
 
     @classmethod
-    def get(cls, state):
+    def get(cls, state: Union['State', str, int]) -> 'State':
         if isinstance(state, cls):
             return state
         elif isinstance(state, str):
diff --git a/codepack/storage/__init__.py b/codepack/storage/__init__.py
index 81a9d3b..a2b600b 100644
--- a/codepack/storage/__init__.py
+++ b/codepack/storage/__init__.py
@@ -1,6 +1,8 @@
-from codepack.storage.storable import Storable
-from codepack.storage.storage import Storage
-from codepack.storage.memory_storage import MemoryStorage
-from codepack.storage.file_storage import FileStorage
-from codepack.storage.mongo_storage import MongoStorage
-from codepack.storage.kafka_storage import KafkaStorage
+from .memory_storage import MemoryStorage
+from .file_storage import FileStorage
+from .mongo_storage import MongoStorage
+from .s3_storage import S3Storage
+from .memory_messenger import MemoryMessenger
+from .kafka_messenger import KafkaMessenger
+from .memory_jobstore import MemoryJobStore
+from .mongo_jobstore import MongoJobStore
diff --git a/codepack/storage/file_storage.py b/codepack/storage/file_storage.py
index 9a55d7a..fd1b495 100644
--- a/codepack/storage/file_storage.py
+++ b/codepack/storage/file_storage.py
@@ -1,18 +1,19 @@
-from codepack.storage import Storage, Storable
+from codepack.storage.storage import Storage
+from codepack.storage.storable import Storable
 from shutil import rmtree
 from glob import glob
 import os
-from typing import Type
+from typing import Type, Union, Optional, Any
 
 
 class FileStorage(Storage):
-    def __init__(self, item_type: Type[Storable] = None, path: str = '.'):
-        super().__init__(item_type=item_type)
+    def __init__(self, item_type: Optional[Type[Storable]] = None, key: str = 'serial_number', path: str = '.') -> None:
+        super().__init__(item_type=item_type, key=key)
         self.path = None
         self.new_path = None
         self.init(path=path)
 
-    def init(self, path: str = '.'):
+    def init(self, path: str = '.') -> None:
         self.path = path
         if os.path.exists(path):
             self.new_path = False
@@ -20,22 +21,22 @@ def init(self, path: str = '.'):
             self.new_path = True
             self.mkdir(path)
 
-    def close(self):
+    def close(self) -> None:
         if self.new_path:
             self.rmdir(self.path)
 
     @staticmethod
-    def mkdir(path: str):
+    def mkdir(path: str) -> None:
         if not os.path.exists(path):
             os.makedirs(path)
 
     @staticmethod
-    def rmdir(path: str):
+    def rmdir(path: str) -> None:
         if os.path.exists(path):
             rmtree(path)
 
     @staticmethod
-    def empty_dir(path: str):
+    def empty_dir(path: str) -> None:
         for item in glob(os.path.join(path, '*')):
             if os.path.isfile(item):
                 os.remove(item)
@@ -43,3 +44,108 @@ def empty_dir(path: str):
                 rmtree(item)
             else:
                 raise NotImplementedError('%s is unknown' % item)  # pragma: no cover
+
+    def exist(self, key: Union[str, list], summary: str = '') -> Union[bool, list]:
+        if isinstance(key, str):
+            path = self.item_type.get_path(key=key, path=self.path)
+            return os.path.exists(path)
+        elif isinstance(key, list):
+            _summary, ret = self._validate_summary(summary=summary)
+            for k in key:
+                path = self.item_type.get_path(key=k, path=self.path)
+                exists = os.path.exists(path)
+                if _summary == 'and' and not exists:
+                    return False
+                elif _summary == 'or' and exists:
+                    return True
+                elif _summary == '':
+                    ret.append(exists)
+            return ret
+        else:
+            raise TypeError(key)  # pragma: no cover
+
+    def remove(self, key: Union[str, list]) -> None:
+        if isinstance(key, str):
+            os.remove(path=self.item_type.get_path(key=key, path=self.path))
+        elif isinstance(key, list):
+            for k in key:
+                path = self.item_type.get_path(key=k, path=self.path)
+                os.remove(path)
+        else:
+            raise TypeError(key)  # pragma: no cover
+
+    def search(self, key: str, value: Any, projection: Optional[list] = None, to_dict: Optional[bool] = None) -> list:
+        ret = list()
+        for filename in glob(self.path + '*.json'):
+            item = self.item_type.from_file(filename)
+            d = item.to_dict()
+            if d[key] != value:
+                continue
+            if projection:
+                ret.append({k: d[k] for k in set(projection).union({self.key})})
+            elif to_dict:
+                ret.append(d)
+            else:
+                ret.append(item)
+        return ret
+
+    def save(self, item: Union[Storable, list], update: bool = False) -> None:
+        if isinstance(item, self.item_type):
+            item_key = getattr(item, self.key)
+            path = item.get_path(key=item_key, path=self.path)
+            if update:
+                if self.exist(key=item_key):
+                    self.remove(key=item_key)
+                item.to_file(path)
+            elif self.exist(key=item_key):
+                raise ValueError('%s already exists' % item_key)
+            else:
+                item.to_file(path)
+        elif isinstance(item, list):
+            for i in item:
+                self.save(item=i, update=update)
+        else:
+            raise TypeError(item)  # pragma: no cover
+
+    def update(self, key: Union[str, list], values: dict) -> None:
+        if len(values) > 0:
+            item = self.load(key=key, to_dict=True)
+            if isinstance(item, dict):
+                if item is not None:
+                    for k, v in values.items():
+                        item[k] = v
+                    self.save(item=self.item_type.from_dict(item), update=True)
+            elif isinstance(item, list):
+                if len(item) > 0:
+                    for i in item:
+                        for k, v in values.items():
+                            i[k] = v
+                    self.save(item=[self.item_type.from_dict(i) for i in item], update=True)
+            else:
+                raise TypeError(type(item))  # pragma: no cover
+
+    def load(self, key: Union[str, list], projection: Optional[list] = None, to_dict: bool = False)\
+            -> Optional[Union[Storable, dict, list]]:
+        if isinstance(key, str):
+            if projection:
+                to_dict = True
+            path = self.item_type.get_path(key=key, path=self.path)
+            if not self.exist(key=key):
+                return None
+            ret_instance = self.item_type.from_file(path)
+            if projection:
+                d = ret_instance.to_dict()
+                return {k: d[k] for k in set(projection).union({self.key})}
+            elif to_dict:
+                return ret_instance.to_dict()
+            else:
+                return ret_instance
+        elif isinstance(key, list):
+            ret = list()
+            for k in key:
+                tmp = self.load(key=k, projection=projection, to_dict=to_dict)
+                if tmp is not None:
+                    ret.append(tmp)
+            return ret
+        else:
+            raise TypeError(key)  # pragma: no cover
diff --git a/codepack/storage/kafka_messenger.py b/codepack/storage/kafka_messenger.py
new file mode 100644
index 0000000..dc5c5a9
--- /dev/null
+++ b/codepack/storage/kafka_messenger.py
@@ -0,0 +1,85 @@
+from codepack.interface.kafka_consumer import KafkaConsumer
+from codepack.interface.kafka_producer import KafkaProducer
+from codepack.storage.messenger import Messenger
+from typing import Union, Optional, Any, Callable
+
+
+class KafkaMessenger(Messenger):
+    def __init__(self, producer: Optional[Union[KafkaProducer, dict]] = None,
+                 consumer: Optional[Union[KafkaConsumer, dict]] = None,
+                 producer_topic: Optional[str] = None, consumer_topic: Optional[str] = None,
+                 producer_config: Optional[dict] = None, consumer_config: Optional[dict] = None) -> None:
+        super().__init__()
+        self.producer = None
+        self.consumer = None
+        self.producer_topic = None
+        self.consumer_topic = None
+        self.new_producer_connection = None
+        self.new_consumer_connection = None
+        self.init(producer=producer, consumer=consumer,
+                  producer_topic=producer_topic, consumer_topic=consumer_topic,
+                  producer_config=producer_config, consumer_config=consumer_config)
+
+    def init(self, producer: Optional[Union[KafkaProducer, dict]] = None,
+             consumer: Optional[Union[KafkaConsumer, dict]] = None,
+             producer_topic: Optional[str] = None, consumer_topic: Optional[str] = None,
+             producer_config: Optional[dict] = None, consumer_config: Optional[dict] = None) -> None:
+        self.init_producer(producer=producer, producer_topic=producer_topic, producer_config=producer_config)
+        self.init_consumer(consumer=consumer, consumer_topic=consumer_topic, consumer_config=consumer_config)
+
+    def init_producer(self, producer: Optional[Union[KafkaProducer, dict]] = None,
+                      producer_topic: Optional[str] = None, producer_config: Optional[dict] = None) -> None:
+        if isinstance(producer, KafkaProducer) or producer is None:
+            self.producer = producer
+            self.new_producer_connection = False
+        elif isinstance(producer, dict):
+            if not producer_config:
+                producer_config = dict()
+            self.producer_topic = producer.pop('topic', None)
+            self.producer = KafkaProducer(producer, **producer_config)
+            self.new_producer_connection = True
+        else:
+            raise TypeError(type(producer))  # pragma: no cover
+        if producer_topic is not None:
+            self.producer_topic = producer_topic
+
+    def init_consumer(self, consumer: Optional[Union[KafkaConsumer, dict]] = None,
+                      consumer_topic: Optional[str] = None, consumer_config: Optional[dict] = None) -> None:
+        if isinstance(consumer, KafkaConsumer) or consumer is None:
+            self.consumer = consumer
+            self.new_consumer_connection = False
+        elif isinstance(consumer, dict):
+            if not consumer_config:
+                consumer_config = dict()
+            self.consumer = KafkaConsumer(consumer, **consumer_config)
+            if 'topic' in consumer:
+                self.consumer_topic = consumer['topic']
+            self.new_consumer_connection = True
+            if consumer_topic is not None:
+                self.consumer_topic = consumer_topic
+        else:
+            raise TypeError(type(consumer))  # pragma: no cover
+
+    def close(self) -> None:
+        self.close_producer()
+        self.close_consumer()
+
+    def close_producer(self) -> None:
+        if self.new_producer_connection:
+            self.producer.close()
+        self.producer = None
+
+    def close_consumer(self) -> None:
+        if self.new_consumer_connection:
+            self.consumer.close()
+        self.consumer = None
+
+    def send(self, item: Any, *args: Any, **kwargs: Any) -> None:
+        assert self.producer is not None
+        self.producer.produce(topic=self.producer_topic, value=item, *args, **kwargs)
+
+    def receive(self, callback: Callable, background: bool = False, interval: float = 1,
+                *args: Any, **kwargs: Any) -> None:
+        assert self.consumer is not None
+        self.consumer.consume(callback=callback, background=background,
+                              timeout_ms=int(interval * 1000), *args, **kwargs)
diff --git a/codepack/storage/kafka_storage.py b/codepack/storage/kafka_storage.py
deleted file mode 100644
index 8932186..0000000
--- a/codepack/storage/kafka_storage.py
+++ /dev/null
@@ -1,52 +0,0 @@
-from codepack.interface import KafkaConsumer, KafkaProducer
-from codepack.storage import Storage, Storable
-from typing import Union
-from typing import Type
-
-
-class KafkaStorage(Storage):
-    def __init__(self, item_type: Type[Storable] = None,
-                 producer: Union[KafkaProducer, dict] = None, consumer: Union[KafkaConsumer, dict] = None,
-                 topic: str = None, producer_config: dict = None, consumer_config: dict = None):
-        super().__init__(item_type=item_type)
-        self.producer = None
-        self.consumer = None
-        self.topic = None
-        self.new_producer_connection = None
-        self.new_consumer_connection = None
-        self.init(producer=producer, consumer=consumer,
-                  topic=topic, producer_config=producer_config, consumer_config=consumer_config)
-
-    def init(self, producer: Union[KafkaProducer, dict] = None, consumer: Union[KafkaConsumer, dict] = None,
-             topic: str = None, producer_config: dict = None, consumer_config: dict = None):
-        self.topic = topic
-        if isinstance(producer, KafkaProducer) or producer is None:
-            self.producer = producer
-            self.new_producer_connection = False
-        elif isinstance(producer, dict):
-            if not producer_config:
-                producer_config = dict()
-            self.producer = KafkaProducer(producer, **producer_config)
-            self.new_producer_connection = True
-        else:
-            raise TypeError(type(producer))
-        if isinstance(consumer, KafkaConsumer) or consumer is None:
-            self.consumer = consumer
-            self.new_consumer_connection = False
-        elif isinstance(consumer, dict):
-            if not consumer_config:
-                consumer_config = dict()
-            self.consumer = KafkaConsumer(consumer, **consumer_config)
-            if 'topic' in consumer:
-                self.topic = consumer['topic']
-            self.new_consumer_connection = True
-        else:
-            raise TypeError(type(consumer))
-
-    def close(self):
-        if self.new_producer_connection:
-            self.producer.close()
-        self.producer = None
-        if self.new_consumer_connection:
-            self.consumer.close()
-        self.consumer = None
diff --git a/codepack/storage/memory_jobstore.py b/codepack/storage/memory_jobstore.py
new file mode 100644
index 0000000..75dc333
--- /dev/null
+++ b/codepack/storage/memory_jobstore.py
@@ -0,0 +1,1 @@
+from apscheduler.jobstores.memory import MemoryJobStore
diff --git a/codepack/storage/memory_messenger.py b/codepack/storage/memory_messenger.py
new file mode 100644
index 0000000..5a10a3e
--- /dev/null
+++ b/codepack/storage/memory_messenger.py
@@ -0,0 +1,75 @@
+from codepack.storage.messenger import Messenger
+from codepack.utils import Looper
+from queue import Queue, Empty
+from collections import namedtuple
+from typing import Any, Callable
+
+
+Message = namedtuple('Message', 'value')
+
+
+class MemoryMessenger(Messenger):
+    queues = dict()
+
+    def __init__(self, topic: str, maxsize: int = 1000, batch: int = 10) -> None:
+        super().__init__()
+        self.topic = topic
+        self.looper = None
+        self.batch = batch
+        self.init(topic=topic, maxsize=maxsize)
+
+    @classmethod
+    def init(cls, topic: str, maxsize: int = 1000) -> None:
+        if topic not in cls.queues:
+            cls.queues[topic] = Queue(maxsize=maxsize)
+
+    @classmethod
+    def __getitem__(cls, item: str) -> Queue:
+        return cls.queues[item]
+
+    def stop(self):
+        if self.looper:
+            self.looper.stop()
+
+    def close(self) -> None:
+        self.stop()
+
+    @classmethod
+    def destroy(cls):
+        topics = list()
+        for tp, q in cls.queues.items():
+            with q.mutex:
+                q.queue.clear()
+            topics.append(tp)
+        for topic in topics:
+            cls.queues.pop(topic, None)
+
+    def send(self, item: Any, *args: Any, **kwargs: Any) -> None:
+        assert self.__class__.queues[self.topic] is not None
+        self.__class__.queues[self.topic].put(item, block=False)
+
+    def receive(self, callback: Callable, background: bool = False, interval: float = 1,
+                *args: Any, **kwargs: Any) -> None:
+        assert self.__class__.queues[self.topic] is not None
+        self.looper = Looper(func=self.fetch, interval=interval, background=background, callback=callback,
+                             *args, **kwargs)
+        self.looper.start()
+
+    def fetch(self, callback: Callable, *args: Any, **kwargs: Any) -> Any:
+        buffer = None
+        msgs = list()
+        for i in range(self.batch):
+            msg = None
+            try:
+                msg = self.__class__.queues[self.topic].get(block=False, *args, **kwargs)
+            except Empty:
+                continue
+            if msg:
+                msgs.append(Message(msg))
+        if len(msgs) > 0:
+            buffer = dict()
+            buffer[self.topic] = msgs
+        if buffer:
+            return callback(buffer)
+        else:
+            return None
diff --git a/codepack/storage/memory_storage.py b/codepack/storage/memory_storage.py
index c58849f..3b9c062 100644
--- a/codepack/storage/memory_storage.py
+++ b/codepack/storage/memory_storage.py
@@ -1,16 +1,113 @@
-from codepack.storage import Storage, Storable
-from typing import Type
+from codepack.storage.storage import Storage
+from typing import TypeVar, Type, Union, Optional, Any
+
+
+Storable = TypeVar('Storable', bound='codepack.storage.storable.Storable')
 
 
 class MemoryStorage(Storage):
-    def __init__(self, item_type: Type[Storable] = None):
-        super().__init__(item_type=item_type)
+    def __init__(self, item_type: Optional[Type[Storable]] = None, key: str = 'serial_number') -> None:
+        super().__init__(item_type=item_type, key=key)
         self.memory = None
         self.init()
 
-    def init(self):
+    def init(self) -> None:
         self.memory = dict()
 
-    def close(self):
+    def close(self) -> None:
         self.memory.clear()
         self.memory = None
+
+    def exist(self, key: Union[str, list], summary: str = '') -> Union[bool, list]:
+        if isinstance(key, str):
+            return key in self.memory.keys()
+        elif isinstance(key, list):
+            _summary, ret = self._validate_summary(summary=summary)
+            for k in key:
+                exists = k in self.memory.keys()
+                if _summary == 'and' and not exists:
+                    return False
+                elif _summary == 'or' and exists:
+                    return True
+                elif _summary == '':
+                    ret.append(exists)
+            return ret
+        else:
+            raise TypeError(key)  # pragma: no cover
+
+    def remove(self, key: Union[str, list]) -> None:
+        if isinstance(key, str):
+            self.memory.pop(key, None)
+        elif isinstance(key, list):
+            for k in key:
+                self.remove(key=k)
+        else:
+            raise TypeError(key)  # pragma: no cover
+
+    def search(self, key: str, value: Any, projection: Optional[list] = None, to_dict: bool = False) -> list:
+        ret = list()
+        for item in self.memory.values():
+            d = item.to_dict()
+            if d[key] != value:
+                continue
+            if projection:
+                ret.append({k: d[k] for k in set(projection).union({self.key})})
+            elif to_dict:
+                ret.append(d)
+            else:
+                ret.append(item)
+        return ret
+
+    def save(self, item: Union[Storable, list], update: bool = False) -> None:
+        if isinstance(item, self.item_type):
+            item_key = getattr(item, self.key)
+            if not update and self.exist(key=item_key):
+                raise ValueError('%s already exists' % item_key)
+            else:
+                self.memory[item_key] = item
+        elif isinstance(item, list):
+            for i in item:
+                self.save(item=i, update=update)
+        else:
+            raise TypeError(item)  # pragma: no cover
+
+    def update(self, key: Union[str, list], values: dict) -> None:
+        if len(values) > 0:
+            item = self.load(key=key, to_dict=True)
+            if isinstance(item, dict):
+                if item is not None:
+                    for k, v in values.items():
+                        item[k] = v
+                    self.save(item=self.item_type.from_dict(item), update=True)
+            elif isinstance(item, list):
+                if len(item) > 0:
+                    for i in item:
+                        for k, v in values.items():
+                            i[k] = v
+                    self.save(item=[self.item_type.from_dict(i) for i in item], update=True)
+            else:
+                raise TypeError(type(item))  # pragma: no cover
+
+    def load(self, key: Union[str, list], projection: Optional[list] = None, to_dict: bool = False)\
+            -> Optional[Union[Storable, dict, list]]:
+        if isinstance(key, str):
+            if self.exist(key=key):
+                item = self.memory[key]
+                d = item.to_dict()
+                if projection:
+                    return {k: d[k] for k in set(projection).union({self.key})}
+                elif to_dict:
+                    return d
+                else:
+                    return item
+            else:
+                return None
+        elif isinstance(key, list):
+            ret = list()
+            for s in key:
+                tmp = self.load(key=s, projection=projection, to_dict=to_dict)
+                if tmp:
+                    ret.append(tmp)
+            return ret
+        else:
+            raise TypeError(type(key))  # pragma: no cover
diff --git a/codepack/storage/messenger.py b/codepack/storage/messenger.py
new file mode 100644
index 0000000..bfdca24
--- /dev/null
+++ b/codepack/storage/messenger.py
@@ -0,0 +1,23 @@
+import abc
+from typing import Any
+
+
+class Messenger(metaclass=abc.ABCMeta):
+    def __init__(self) -> None:
+        pass
+
+    @abc.abstractmethod
+    def init(self, *args: Any, **kwargs: Any) -> None:
+        """initialize messenger"""
+
+    @abc.abstractmethod
+    def close(self) -> None:
+        """close messenger"""
+
+    @abc.abstractmethod
+    def send(self, item: Any, *args: Any, **kwargs: Any) -> Any:
+        """send item"""
+
+    @abc.abstractmethod
+    def receive(self, *args: Any, **kwargs: Any) -> Any:
+        """receive item"""
diff --git a/codepack/scheduler/jobstore/mongo_jobstore.py b/codepack/storage/mongo_jobstore.py
similarity index 63%
rename from codepack/scheduler/jobstore/mongo_jobstore.py
rename to codepack/storage/mongo_jobstore.py
index 52ce9a7..a765717 100644
--- a/codepack/scheduler/jobstore/mongo_jobstore.py
+++ b/codepack/storage/mongo_jobstore.py
@@ -1,20 +1,28 @@
+from codepack.interface.mongodb import MongoDB
 from apscheduler.jobstores.mongodb import *
 from datetime import datetime, timezone
+from typing import Any, TypeVar, Union
+
+
+Job = TypeVar('Job', bound='apscheduler.job.Job')
 
 
 class MongoJobStore(MongoDBJobStore):
-    def __init__(self, db,
-                 collection,
-                 client=None,
-                 pickle_protocol=pickle.HIGHEST_PROTOCOL,
-                 **connect_args):
+    def __init__(self, mongodb: Union[MongoDB, dict], db: str, collection: str,
+                 pickle_protocol: int = pickle.HIGHEST_PROTOCOL, *args: Any, **kwargs: Any) -> None:
+        if isinstance(mongodb, MongoDB):
+            _client = mongodb.session
+        elif isinstance(mongodb, dict):
+            _mongodb = MongoDB(mongodb, *args, **kwargs)
+            _client = _mongodb.session
+        else:
+            raise TypeError(type(mongodb))  # pragma: no cover
         super().__init__(database=db,
                          collection=collection,
-                         client=client,
-                         pickle_protocol=pickle_protocol,
-                         **connect_args)
+                         client=_client,
+                         pickle_protocol=pickle_protocol)
 
-    def add_job(self, job):
+    def add_job(self, job: Job) -> None:
         utc_timestamp = datetime_to_utc_timestamp(job.next_run_time)
         try:
             self.collection.insert_one({
@@ -30,7 +38,7 @@ def add_job(self, job):
         except DuplicateKeyError:
             raise ConflictingIdError(job.id)
 
-    def update_job(self, job):
+    def update_job(self, job: Job) -> None:
         utc_timestamp = datetime_to_utc_timestamp(job.next_run_time)
         changes = {
             'last_run_time': datetime.now(timezone.utc).timestamp(),
diff --git a/codepack/storage/mongo_storage.py b/codepack/storage/mongo_storage.py
index 6049152..20671e6 100644
--- a/codepack/storage/mongo_storage.py
+++ b/codepack/storage/mongo_storage.py
@@ -1,20 +1,22 @@
-from codepack.interface import MongoDB
-from codepack.storage import Storage, Storable
-from typing import Union
-from typing import Type
+from codepack.interface.mongodb import MongoDB
+from codepack.storage.storage import Storage
+from codepack.storage.storable import Storable
+from typing import Type, Optional, Union, Any
 
 
 class MongoStorage(Storage):
-    def __init__(self, item_type: Type[Storable] = None,
-                 mongodb: Union[MongoDB, dict] = None, db: str = None, collection: str = None, *args, **kwargs):
-        super().__init__(item_type=item_type)
+    def __init__(self, item_type: Optional[Type[Storable]] = None, key: str = 'serial_number',
+                 mongodb: Optional[Union[MongoDB, dict]] = None,
+                 db: Optional[str] = None, collection: Optional[str] = None, *args: Any, **kwargs: Any) -> None:
+        super().__init__(item_type=item_type, key=key)
         self.mongodb = None
         self.db = None
         self.collection = None
         self.new_connection = None
         self.init(mongodb=mongodb, db=db, collection=collection, *args, **kwargs)
 
-    def init(self, mongodb: Union[MongoDB, dict] = None, db: str = None, collection: str = None, *args, **kwargs):
+    def init(self, mongodb: Optional[Union[MongoDB, dict]] = None,
+             db: Optional[str] = None, collection: Optional[str] = None, *args: Any, **kwargs: Any) -> None:
         self.db = db
         self.collection = collection
         if isinstance(mongodb, MongoDB):
@@ -24,9 +26,103 @@ def init(self, mongodb: Union[MongoDB, dict] = None, db: str = None, collection:
             self.mongodb = MongoDB(mongodb, *args, **kwargs)
             self.new_connection = True
         else:
-            raise TypeError(type(mongodb))
+            raise TypeError(type(mongodb))  # pragma: no cover
 
-    def close(self):
+    def close(self) -> None:
         if self.new_connection:
             self.mongodb.close()
         self.mongodb = None
+
+    def exist(self, key: Union[str, list], summary: str = '') -> Union[bool, list]:
+        if isinstance(key, str):
+            return self.mongodb[self.db][self.collection].count_documents({'_id': key}) > 0
+        elif isinstance(key, list):
+            _summary, _ = self._validate_summary(summary=summary)
+            if _summary == 'and':
+                return self.mongodb[self.db][self.collection].count_documents({'_id': {'$in': key}}) == len(key)
+            elif _summary == 'or':
+                return self.mongodb[self.db][self.collection].count_documents({'_id': {'$in': key}}) > 0
+            elif _summary == '':
+                tmp = self.mongodb[self.db][self.collection].find({'_id': {'$in': key}}, projection={'_id': True})
+                existing_keys = {k['_id'] for k in tmp}
+                return [True if k in existing_keys else False for k in key]
+        else:
+            raise TypeError(key)  # pragma: no cover
+
+    def remove(self, key: Union[str, list]) -> None:
+        if isinstance(key, str):
+            self.mongodb[self.db][self.collection].delete_one({'_id': key})
+        elif isinstance(key, list):
+            self.mongodb[self.db][self.collection].delete_many({'_id': {'$in': key}})
+        else:
+            raise TypeError(key)  # pragma: no cover
+
+    def search(self, key: str, value: Any, projection: Optional[list] = None, to_dict: bool = False) -> list:
+        if projection:
+            to_dict = True
+            _projection = {k: True for k in projection}
+            _projection[self.key] = True
+            if '_id' not in projection and self.key != '_id':
+                _projection['_id'] = False
+        else:
+            _projection = projection
+        search_result = self.mongodb[self.db][self.collection].find({key: value}, projection=_projection)
+        if not search_result:
+            return list()
+        if to_dict:
+            return list(search_result)
+        else:
+            return [self.item_type.from_dict(d) for d in search_result]
+
+    def save(self, item: Union[Storable, list], update: bool = False) -> None:
+        if isinstance(item, self.item_type):
+            item_key = getattr(item, self.key)
+            if not update and self.exist(key=item_key):
+                raise ValueError('%s already exists' % item_key)
+            else:
+                d = item.to_dict()
+                d.pop('_id', None)
+                self.mongodb[self.db][self.collection].update_one({'_id': item_key}, {'$set': d}, upsert=True)
+        elif isinstance(item, list):
+            for i in item:
+                self.save(item=i, update=update)
+        else:
+            raise TypeError(item)  # pragma: no cover
+
+    def update(self, key: Union[str, list], values: dict) -> None:
+        if len(values) > 0:
+            if isinstance(key, str):
+                self.mongodb[self.db][self.collection].update_one({'_id': key}, {'$set': values})
+            elif isinstance(key, list):
+                self.mongodb[self.db][self.collection].update_many({'_id': {'$in': key}}, {'$set': values})
+            else:
+                raise TypeError(type(key))  # pragma: no cover
+
+    def load(self, key: Union[str, list], projection: Optional[list] = None, to_dict: bool = False)\
+            -> Optional[Union[Storable, dict, list]]:
+        if projection:
+            to_dict = True
+            _projection = {k: True for k in projection}
+            _projection[self.key] = True
+            if '_id' not in projection and self.key != '_id':
+                _projection['_id'] = False
+        else:
+            _projection = projection
+        if isinstance(key, str):
+            d = self.mongodb[self.db][self.collection].find_one({'_id': key}, projection=_projection)
+            if not d:
+                return None
+            elif to_dict:
+                return d
+            else:
+                return self.item_type.from_dict(d)
+        elif isinstance(key, list):
+            tmp = self.mongodb[self.db][self.collection].find({'_id': {'$in': key}}, projection=_projection)
+            if not tmp:
+                return None
+            elif to_dict:
+                return list(tmp)
+            else:
+                return [self.item_type.from_dict(d) for d in tmp]
+        else:
+            raise TypeError(type(key))  # pragma: no cover
diff --git a/codepack/storage/s3_storage.py b/codepack/storage/s3_storage.py
new file mode 100644
index 0000000..263c63f
--- /dev/null
+++ b/codepack/storage/s3_storage.py
@@ -0,0 +1,146 @@
+from codepack.interface.s3 import S3
+from codepack.storage.storage import Storage
+from codepack.storage.storable import Storable
+from typing import Type, Union, Optional, Any
+from posixpath import join
+
+
+class S3Storage(Storage):
+    def __init__(self, item_type: Optional[Type[Storable]] = None, key: str = 'serial_number',
+                 s3: Optional[Union[S3, dict]] = None,
+                 bucket: Optional[str] = None, path: str = '', *args: Any, **kwargs: Any) -> None:
+        super().__init__(item_type=item_type, key=key)
+        self.s3 = None
+        self.bucket = None
+        self.path = None
+        self.new_connection = None
+        self.init(s3=s3, bucket=bucket, path=path, *args, **kwargs)
+
+    def init(self, s3: Optional[Union[S3, dict]] = None,
+             bucket: Optional[str] = None, path: Optional[str] = None, *args: Any, **kwargs: Any) -> None:
+        self.bucket = bucket
+        self.path = path
+        if isinstance(s3, S3):
+            self.s3 = s3
+            self.new_connection = False
+        elif isinstance(s3, dict):
+            self.s3 = S3(s3, *args, **kwargs)
+            self.new_connection = True
+        else:
+            raise TypeError(type(s3))
+
+    def close(self) -> None:
+        if self.new_connection:
+            self.s3.close()
+        self.s3 = None
+
+    def exist(self, key: Union[str, list], summary: str = '') -> Union[bool, list]:
+        if isinstance(key, str):
+            path = self.item_type.get_path(key=key, path=self.path, posix=True)
+            return self.s3.exist(bucket=self.bucket, key=path)
+        elif isinstance(key, list):
+            _summary, ret = self._validate_summary(summary=summary)
+            for k in key:
+                exists = self.exist(key=k, summary=summary)
+                if _summary == 'and' and not exists:
+                    return False
+                elif _summary == 'or' and exists:
+                    return True
+                elif _summary == '':
+                    ret.append(exists)
+            return ret
+        else:
+            raise TypeError(key)
+
+    def remove(self, key: Union[str, list]) -> None:
+        if isinstance(key, str):
+            path = self.item_type.get_path(key=key, path=self.path, posix=True)
+            self.s3.delete(bucket=self.bucket, key=path)
+        elif isinstance(key, list):
+            for k in key:
+                self.remove(key=k)
+        else:
+            raise TypeError(key)
+
+    def search(self, key: str, value: Any, projection: Optional[list] = None, to_dict: bool = False) -> list:
+        ret = list()
+        if projection:
+            to_dict = True
+        all_obj_info = self.s3.list_objects(bucket=self.bucket, prefix=join(self.path, ''))
+        all_obj_keys = [obj['Key'] for obj in all_obj_info]
+        for k in all_obj_keys:
+            try:
+                instance = self.item_type.from_json(self.s3.download(bucket=self.bucket, key=k))
+                d = instance.to_dict()
+                if d[key] == value:
+                    if projection:
+                        ret.append({k: d[k] for k in set(projection).union({self.key})})
+                    elif to_dict:
+                        ret.append(d)
+                    else:
+                        ret.append(instance)
+            except Exception:
+                continue
+        return ret
+
+    def save(self, item: Union[Storable, list], update: bool = False) -> None:
+        if isinstance(item, self.item_type):
+            item_key = getattr(item, self.key)
+            path = item.get_path(key=item_key, path=self.path, posix=True)
+            if update:
+                if self.exist(key=item_key):
+                    self.remove(key=item_key)
+                self.s3.upload(bucket=self.bucket, key=path, data=item.to_json())
+            elif self.exist(key=item_key):
+                raise ValueError('%s already exists' % item_key)
+            else:
+                self.s3.upload(bucket=self.bucket, key=path, data=item.to_json())
+        elif isinstance(item, list):
+            for i in item:
+                self.save(item=i, update=update)
+        else:
+            raise TypeError(item)
+
+    def update(self, key: Union[str, list], values: dict) -> None:
+        if len(values) > 0:
+            item = self.load(key=key, to_dict=True)
+            if isinstance(item, dict):
+                if item is not None:
+                    for k, v in values.items():
+                        item[k] = v
+                    self.save(item=self.item_type.from_dict(item), update=True)
+            elif isinstance(item, list):
+                if len(item) > 0:
+                    for i in item:
+                        for k, v in values.items():
+                            i[k] = v
+                    self.save(item=[self.item_type.from_dict(i) for i in item], update=True)
+            else:
+                raise TypeError(type(item))  # pragma: no cover
+
+    def load(self, key: Union[str, list], projection: Optional[list] = None, to_dict: bool = False)\
+            -> Optional[Union[Storable, dict, list]]:
+        if isinstance(key, str):
+            if projection:
+                to_dict = True
+            path = self.item_type.get_path(key=key, path=self.path, posix=True)
+            ret_json = self.s3.download(bucket=self.bucket, key=path)
+            if ret_json is None:
+                return None
+            ret_instance = self.item_type.from_json(ret_json)
+            if projection:
+                d = ret_instance.to_dict()
+                return {k: d[k] for k in set(projection).union({self.key})}
+            elif to_dict:
+                return ret_instance.to_dict()
+            else:
+                return ret_instance
+        elif isinstance(key, list):
+            ret = list()
+            for k in key:
+                tmp = self.load(key=k, projection=projection, to_dict=to_dict)
+                if tmp is not None:
+                    ret.append(tmp)
+            return ret
+        else:
+            raise TypeError(key)
diff --git a/codepack/storage/storable.py b/codepack/storage/storable.py
index 65c6a6d..95da29e 100644
--- a/codepack/storage/storable.py
+++ b/codepack/storage/storable.py
@@ -4,47 +4,57 @@
 import dill
 from datetime import datetime
 from bson import json_util
+import os
+from posixpath import join as posixpath_join
+from typing import Any, Optional, Union, TypeVar
+
+
+MongoDB = TypeVar('MongoDB', bound='codepack.interface.mongodb.MongoDB')
+MongoClient = TypeVar('MongoClient', bound='pymongo.mongo_client.MongoClient')
 
 
 class Storable(metaclass=abc.ABCMeta):
-    def __init__(self, id=None, serial_number=None):
+    def __init__(self, id: Optional[str] = None, serial_number: Optional[str] = None,
+                 *args: Any, **kwargs: Any) -> None:
         """initialize instance"""
         self.id = id
         self.serial_number = serial_number if serial_number else self.generate_serial_number()
 
-    def generate_serial_number(self):
+    def generate_serial_number(self) -> str:
         return (str(id(self)) + str(datetime.now().timestamp())).replace('.', '')
 
-    def to_json(self):
+    def to_json(self) -> str:
         return json.dumps(self.to_dict(), default=json_util.default)
 
     @classmethod
-    def from_json(cls, j):
+    def from_json(cls, j: Union[str, bytes]) -> 'Storable':
         d = json.loads(j, object_hook=json_util.object_hook)
         return cls.from_dict(d)
 
-    def to_binary(self):
+    def to_binary(self) -> bson.binary.Binary:
         return bson.Binary(dill.dumps(self))
 
     @classmethod
-    def from_binary(cls, b):
+    def from_binary(cls, b: bson.binary.Binary) -> 'Storable':
         return dill.loads(b)
 
-    def to_file(self, path):
+    def to_file(self, path: str) -> None:
         with open(path, 'w+') as f:
             f.write(self.to_json())
 
     @classmethod
-    def from_file(cls, path):
+    def from_file(cls, path: str) -> 'Storable':
         with open(path, 'r') as f:
             ret = cls.from_json(f.read())
         return ret
 
-    def to_db(self, mongodb, db, collection, *args, **kwargs):
+    def to_db(self, mongodb: Union[MongoDB, MongoClient], db: str, collection: str,
+              *args: Any, **kwargs: Any) -> None:
         mongodb[db][collection].insert_one(self.to_dict(), *args, **kwargs)
 
     @classmethod
-    def from_db(cls, id, mongodb, db, collection, *args, **kwargs):
+    def from_db(cls, id: id, mongodb: Union[MongoDB, MongoClient], db: str, collection: str,
+                *args: Any, **kwargs: Any) -> Optional['Storable']:
         d = mongodb[db][collection].find_one({'_id': id}, *args, **kwargs)
         if d is None:
             return d
@@ -52,14 +62,17 @@ def from_db(cls, id, mongodb, db, collection, *args, **kwargs):
             return cls.from_dict(d)
 
     @abc.abstractmethod
-    def to_dict(self):
+    def to_dict(self) -> dict:
         """convert to dict"""
 
     @classmethod
     @abc.abstractmethod
-    def from_dict(cls, d):
+    def from_dict(cls, d: dict) -> 'Storable':
         """create instance from dict"""
 
     @classmethod
-    def get_path(cls, serial_number, path='./'):
-        return '%s%s.json' % (path, serial_number)
+    def get_path(cls, key: str, path: str = './', posix: bool = False) -> str:
+        if posix:
+            return posixpath_join(path, '%s.json' % key)
+        else:
+            return os.path.join(path, '%s.json' % key)
diff --git a/codepack/storage/storage.py b/codepack/storage/storage.py
index 6ade636..96cccdc 100644
--- a/codepack/storage/storage.py
+++ b/codepack/storage/storage.py
@@ -1,18 +1,57 @@
 import abc
-from codepack.storage import Storable
-from typing import Type
+from codepack.storage.storable import Storable
+from typing import Any, Type, Optional, Union
 
 
 class Storage(metaclass=abc.ABCMeta):
-    def __init__(self, item_type: Type[Storable] = None):
+    def __init__(self, item_type: Optional[Type[Storable]] = None, key: str = 'serial_number') -> None:
         if item_type and not issubclass(item_type, Storable):
             raise TypeError(type(item_type))
         self.item_type = item_type
+        self.key = key
 
     @abc.abstractmethod
-    def init(self, *args, **kwargs):
+    def init(self, *args: Any, **kwargs: Any) -> None:
         """initialize storage"""
 
     @abc.abstractmethod
-    def close(self):
+    def close(self) -> None:
         """close storage"""
+
+    @abc.abstractmethod
+    def exist(self, key: Union[str, list], summary: str = '') -> Union[bool, list]:
+        """check if item with given key exists"""
+
+    @abc.abstractmethod
+    def remove(self, key: Union[str, list]) -> None:
+        """remove item with given key"""
+
+    @abc.abstractmethod
+    def search(self, key: str, value: Any, projection: Optional[list] = None, to_dict: bool = False) -> list:
+        """search by key and value"""
+
+    @abc.abstractmethod
+    def save(self, item: Union[Storable, list], update: bool = False) -> None:
+        """save item"""
+
+    @abc.abstractmethod
+    def update(self, key: Union[str, list], values: dict) -> None:
+        """update item with given key"""
+
+    @abc.abstractmethod
+    def load(self, key: Union[str, list], projection: Optional[list] = None, to_dict: bool = False)\
+            -> Optional[Union[Storable, dict, list]]:
+        """load item with given key"""
+
+    @staticmethod
+    def _validate_summary(summary: str) -> tuple:
+        op = summary.lower()
+        if op == '':
+            init = list()
+        elif op == 'and':
+            init = True
+        elif op == 'or':
+            init = False
+        else:
+            raise ValueError('%s is unknown' % summary)
+        return op, init
diff --git a/codepack/utils/__init__.py b/codepack/utils/__init__.py
index 060568a..1f16de9 100644
--- a/codepack/utils/__init__.py
+++ b/codepack/utils/__init__.py
@@ -1,2 +1,2 @@
 from codepack.utils.looper import Looper
-from codepack.utils.singleton import Singleton
+from codepack.utils.common import Common
diff --git a/codepack/utils/common.py b/codepack/utils/common.py
new file mode 100644
index 0000000..ea85fa9
--- /dev/null
+++ b/codepack/utils/common.py
@@ -0,0 +1,40 @@
+from collections.abc import Callable
+from typing import Any, Optional, KeysView, ValuesView, ItemsView
+
+
+class Common:
+    def __init__(self) -> None:
+        self.variables = dict()
+        self._destroy = dict()
+
+    def __getitem__(self, item: str) -> Any:
+        return self.variables[item]
+
+    def __getattr__(self, item: str) -> Any:
+        return self.__getitem__(item)
+
+    def keys(self) -> KeysView:
+        return self.variables.keys()
+
+    def values(self) -> ValuesView:
+        return self.variables.values()
+
+    def items(self) -> ItemsView:
+        return self.variables.items()
+
+    def add(self, key: str, value: Any, destroy: Optional[Callable] = None) -> None:
+        self.variables[key] = value
+        self._destroy[key] = destroy
+
+    def remove(self, key: str) -> Optional[Any]:
+        ret = None
+        tmp = self.variables.pop(key)
+        destroy = self._destroy.pop(key)
+        if destroy and isinstance(destroy, Callable):
+            ret = destroy(tmp)
+        return ret
+
+    def clear(self) -> None:
+        keys = list(self.keys())
+        for key in keys:
+            self.remove(key)
diff --git a/codepack/utils/looper.py b/codepack/utils/looper.py
index a4fd301..8e76b0f 100644
--- a/codepack/utils/looper.py
+++ b/codepack/utils/looper.py
@@ -1,58 +1,72 @@
 import threading
 import time
+from typing import Any, Callable
 
 
 class Looper:
-    def __init__(self, func, interval=1, daemon=True, *args, **kwargs):
+    def __init__(self, func: Callable, interval: float = 1, background: bool = True, daemon: bool = True,
+                 *args: Any, **kwargs: Any) -> None:
         super().__init__()
         self.func = None
         self.interval = None
         self.running = False
         self.thread = None
         self.daemon = daemon
+        self.background = background
         self.set_interval(interval=interval)
         self.args = args
         self.kwargs = kwargs
         self.set_func(func=func)
 
-    def make_thread(self, func, *args, **kwargs):
+    def make_thread(self, func: Callable, *args: Any, **kwargs: Any) -> threading.Thread:
         self.thread = threading.Thread(target=self.loop, args=(func, *args), kwargs=kwargs, daemon=self.daemon)
         return self.thread
 
-    def set_interval(self, interval):
+    def set_interval(self, interval: float) -> None:
         self.interval = interval
 
-    def set_func(self, func):
+    def set_func(self, func: Callable) -> None:
         self.func = func
 
-    def loop(self, func, *args, **kwargs):
+    def loop(self, func: Callable, *args: Any, **kwargs: Any) -> None:
         try:
             while self.running:
-                time.sleep(self.interval)
-                func(*args, **kwargs)
+                try:
+                    if self.interval > 0:
+                        time.sleep(self.interval)
+                    func(*args, **kwargs)
+                except KeyboardInterrupt:
+                    break
         except Exception as e:
             raise e
         finally:
             self.running = False
 
-    def is_alive(self):
-        return self.thread.is_alive() if self.thread else False
+    def is_alive(self) -> bool:
+        if self.background:
+            return self.thread.is_alive() if self.thread else False
+        else:
+            return False
 
-    def is_running(self):
+    def is_running(self) -> bool:
         return self.running
 
-    def start(self):
-        if self.is_running() or self.is_alive():
+    def start(self) -> None:
+        if self.background and (self.is_running() or self.is_alive()):
             raise Exception('Thread is already running')
         self.running = True
-        self.make_thread(func=self.func, *self.args, **self.kwargs)
-        return self.thread.start()
+        if self.background:
+            self.make_thread(func=self.func, *self.args, **self.kwargs)
+            self.thread.start()
+        else:
+            self.loop(func=self.func, *self.args, **self.kwargs)
 
-    def restart(self):
-        if self.is_running() or self.is_alive():
+    def restart(self) -> None:
+        if self.background and (self.is_running() or self.is_alive()):
             self.stop()
-        return self.start()
+        self.start()
 
-    def stop(self):
+    def stop(self) -> None:
         self.running = False
-        return self.thread.join()
+        if self.background:
+            self.thread.join()
diff --git a/codepack/utils/singleton.py b/codepack/utils/singleton.py
deleted file mode 100644
index ce4b036..0000000
--- a/codepack/utils/singleton.py
+++ /dev/null
@@ -1,6 +0,0 @@
-class Singleton(object):
-    @classmethod
-    def __new__(cls, *args, **kwargs):
-        if not hasattr(cls, "__instance__"):
-            cls.__instance__ = super().__new__(cls)
-        return cls.__instance__
diff --git a/config/alias.ini b/config/alias.ini
deleted file mode 100644
index 8696c29..0000000
--- a/config/alias.ini
+++ /dev/null
@@ -1,19 +0,0 @@
-[alias]
-code = codepack.code.Code
-codepack = codepack.codepack.CodePack
-argpack = codepack.argpack.argpack.ArgPack
-code_snapshot = codepack.snapshot.code_snapshot.CodeSnapshot
-codepack_snapshot = codepack.snapshot.codepack_snapshot.CodePackSnapshot
-delivery = codepack.delivery.delivery.Delivery
-memory_storage_service = codepack.service.storage_service.memory_storage_service.MemoryStorageService
-file_storage_service = codepack.service.storage_service.file_storage_service.FileStorageService
-mongo_storage_service = codepack.service.storage_service.mongo_storage_service.MongoStorageService
-memory_delivery_service = codepack.service.delivery_service.memory_delivery_service.MemoryDeliveryService
-file_delivery_service = codepack.service.delivery_service.file_delivery_service.FileDeliveryService
-mongo_delivery_service = codepack.service.delivery_service.mongo_delivery_service.MongoDeliveryService
-memory_snapshot_service = codepack.service.snapshot_service.memory_snapshot_service.MemorySnapshotService
-file_snapshot_service = codepack.service.snapshot_service.file_snapshot_service.FileSnapshotService
-mongo_snapshot_service = codepack.service.snapshot_service.mongo_snapshot_service.MongoSnapshotService
-mongo_scheduler = codepack.scheduler.mongo_scheduler.MongoScheduler
-supervisor = codepack.employee.supervisor.Supervisor
-worker = codepack.employee.worker.Worker
diff --git a/config/codepack.ini b/config/codepack.ini
index ff2ca91..933159e 100644
--- a/config/codepack.ini
+++ b/config/codepack.ini
@@ -1,9 +1,3 @@
-[conn]
-path = config/conn.ini
-
-[alias]
-path = config/alias.ini
-
 [code]
 source = mongodb
 db = codepack
@@ -20,9 +14,9 @@ db = codepack
 collection = argpack
 
 [delivery]
-source = mongodb
-db = codepack
-collection = cache
+source = s3
+bucket = codepack
+path = delivery
 
 [code_snapshot]
 source = mongodb
@@ -34,9 +28,12 @@ source = mongodb
 db = codepack
 collection = codepack_snapshot
 
+[callback]
+source = file
+; path = scripts
+
 [apiserver]
-scheduler = self
-supervisor = self
+scheduler = http://localhost:8001
 
 [scheduler]
 source = mongodb
@@ -50,7 +47,85 @@ topic = codepack
 group_id = codepack_worker
 interval = 1
 supervisor = http://localhost:8000
+; script_path = scripts/run_snapshot.py
+logger = worker-logger
+background = False
 
 [supervisor]
 source = kafka
 topic = codepack
+
+[docker_manager]
+source = docker
+; path = scripts
+run_opt = {"dns": ["8.8.8.8"]}
+
+[interpreter_manager]
+path = /home/codepack/anaconda3/envs
+run_opt = {}
+
+[logger]
+; config_path = logging.json
+name = error-logger
+log_dir = logs
+
+[ssh]
+ssh_host = 1.2.3.4
+ssh_port = 22
+ssh_username = ?
+ssh_password = ?
+
+[mongodb]
+host = localhost,?
+port = 27017
+username = ?
+password = ?
+replicaSet = ?
+
+[mysql]
+host = localhost
+port = 3306
+user = ?
+passwd = ?
+charset = utf8
+cursorclass = pymysql.cursors.Cursor
+; cursorclass = pymysql.cursors.DictCursor
+
+[mssql]
+host = localhost
+port = 1433
+user = ?
+password = ?
+charset = utf8
+as_dict = False
+
+[oracledb]
+host = localhost
+port = 1621
+service_name = ?
+user = ?
+password = ?
+as_dict = False
+
+[kafka]
+bootstrap_servers=?:9092,?:9092,?:9092
+
+[rest_api]
+my_service = https://?:?/?
+
+[dynamodb]
+service_name = dynamodb
+region_name = ?
+endpoint_url = https://?
+aws_access_key_id = ?
+aws_secret_access_key = ?
+
+[docker]
+base_url = unix://var/run/docker.sock
+
+[s3]
+service_name = s3
+region_name = ?
+endpoint_url = https://?
+aws_access_key_id = ?
+aws_secret_access_key = ?
diff --git a/config/conn.ini b/config/conn.ini
deleted file mode 100644
index 6beb726..0000000
--- a/config/conn.ini
+++ /dev/null
@@ -1,50 +0,0 @@
-[ssh]
-ssh_host = localhost
-ssh_port = 22
-ssh_username = ?
-ssh_password = ?
-
-[mongodb]
-host = localhost,?
-port = 27017
-username = ?
-password = ?
-replicaSet = ?
-
-[mysql]
-host = localhost
-port = 3306
-user = ?
-passwd = ?
-charset = utf8
-cursorclass = pymysql.cursors.Cursor
-; cursorclass = pymysql.cursors.DictCursor
-
-[mssql]
-host = localhost
-port = 1433
-user = ?
-password = ?
-charset = utf8
-as_dict = False
-
-[oracledb]
-host = localhost
-port = 1621
-service_name = ?
-user = ?
-password = ?
-as_dict = False
-
-[kafka]
-bootstrap_servers=?:9092,?:9092,?:9092
-
-[rest_api]
-my_service = https://?:?/?
-
-[dynamodb]
-service_name = dynamodb
-region_name = ?
-endpoint_url = https://?
-aws_access_key_id = ?
-aws_secret_access_key = ?
diff --git a/config/jupyter_notebook_config.py b/config/jupyter_notebook_config.py
new file mode 100644
index 0000000..14399df
--- /dev/null
+++ b/config/jupyter_notebook_config.py
@@ -0,0 +1,8 @@
+c.NotebookApp.notebook_dir = '/opt/codepack/jupyter/notebook'
+c.NotebookApp.password_required = False
+c.NotebookApp.allow_root = True
+c.NotebookApp.open_browser = False
+c.NotebookApp.port = 8888
+c.NotebookApp.ip = '0.0.0.0'
+# c.NotebookApp.password = 'jupyter'
+c.NotebookApp.token = 'jupyter'
diff --git a/config/logging.json b/config/logging.json
new file mode 100644
index 0000000..72304d1
--- /dev/null
+++ b/config/logging.json
@@ -0,0 +1,46 @@
+{
+    "version": 1,
+    "formatters": {
+        "default": {
+            "format": "%(asctime)s %(levelname)-8s %(module)s.%(funcName)s[%(lineno)s]: %(message)s"
+        }
+    },
+    "handlers": {
+        "console": {
+            "class": "logging.StreamHandler",
+            "level": "INFO",
+            "formatter": "default"
+        },
+        "error-file": {
+            "class": "logging.handlers.TimedRotatingFileHandler",
+            "level": "ERROR",
+            "formatter": "default",
+            "filename": "error.log",
+            "when": "midnight"
+        },
+        "worker": {
+            "class": "logging.handlers.TimedRotatingFileHandler",
+            "level": "INFO",
+            "formatter": "default",
+            "filename": "worker.log",
+            "when": "midnight"
+        }
+    },
+    "loggers": {
+        "default-logger": {
+            "level": "INFO",
+            "handlers": ["console"],
+            "propagate": true
+        },
+        "error-logger": {
+            "level": "ERROR",
+            "handlers": ["console", "error-file"],
+            "propagate": true
+        },
+        "worker-logger": {
+            "level": "INFO",
+            "handlers": ["console", "worker"],
+            "propagate": true
+        }
+    }
+}
diff --git a/entry_point.sh b/entry_point.sh
new file mode 100644
index 0000000..788b60d
--- /dev/null
+++ b/entry_point.sh
@@ -0,0 +1,19 @@
+#!/usr/bin/env bash
+
+if [[ $# == 0 ]]; then
+  exec python
+else
+  USER_COMMAND=$1
+  USER_OPTION="${@:2}"
+  if [[ ${USER_COMMAND} == "apiserver" ]]; then
+    exec uvicorn app.apiserver.main:app ${USER_OPTION}
+  elif [[ ${USER_COMMAND} == "scheduler" ]]; then
+    exec uvicorn app.scheduler.main:app ${USER_OPTION}
+  elif [[ ${USER_COMMAND} == "worker" ]]; then
+    exec python app/worker/main.py ${USER_OPTIONS}
+  elif [[ ${USER_COMMAND} == "jupyter" ]]; then
+    exec jupyter notebook --config /opt/codepack/config/jupyter_notebook_config.py ${USER_OPTION}
+  else
+    exec ${USER_COMMAND} ${USER_OPTION}
+  fi
+fi
diff --git a/extra-requirements.txt b/extra-requirements.txt
new file mode 100644
index 0000000..25b196e
--- /dev/null
+++ b/extra-requirements.txt
@@ -0,0 +1,4 @@
+codepack
+jupyter
+uvicorn[standard]
+fastapi
diff --git a/requirements.txt b/requirements.txt
index 03c26f3..c503f30 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,6 +1,7 @@
 dill==0.3.4
 pymongo==3.12.1
 numpy
+pandas
 sshtunnel==0.4.0
 PyMySQL==1.0.2
 pymssql==2.2.2
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..9086639
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,37 @@
+import setuptools
+
+
+with open("README.rst", "r") as f:
+    long_description = f.read()
+
+
+with open("requirements.txt", "r") as f:
+    requirements = [x.strip() for x in f.readlines()]
+
+
+setuptools.setup(
+    name="codepack",
+    version="0.1.0",
+    author="ihnokim",
+    author_email="ihnokim58@gmail.com",
+    description="Codepack is the package for making workflows with Python",
+    long_description=long_description,
+    url="https://github.com/ihnokim/codepack",
+    packages=setuptools.find_packages(),
+    keywords=["codepack", "workflow", "pipeline"],
+    install_requires=requirements,
+    package_data={'codepack': ['config/default/*']},
+    data_files=[("codepack/config", ["config/logging.json", "config/codepack.ini"])],
+    classifiers=[
+        "Programming Language :: Python",
+        "Programming Language :: Python :: 3",
+        "Programming Language :: Python :: 3 :: Only",
+        "Programming Language :: Python :: 3.6",
+        "Programming Language :: Python :: 3.7",
+        "Programming Language :: Python :: 3.8",
+        "Programming Language :: Python :: 3.9",
+        "License :: OSI Approved :: MIT License",
+        "Operating System :: OS Independent"
+    ],
+    python_requires=">=3.6",
+)


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 5881dc8..b0bdeb2 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -9,7 +9,7 @@ jobs:
     strategy:
       max-parallel: 4
       matrix:
-        python-version: [3.6, 3.7, 3.8, 3.9]
+        python-version: ["3.6", "3.7", "3.8", "3.9"]
       fail-fast: false
     steps:
     - uses: actions/checkout@v2
diff --git a/config/test.ini b/config/test.ini
index 081b1c2..2b8456d 100644
--- a/config/test.ini
+++ b/config/test.ini
@@ -1,9 +1,3 @@
-[conn]
-path = config/test_conn.ini
-
-[alias]
-path = config/alias.ini
-
 [code]
 source = memory
 
@@ -22,9 +16,12 @@ source = memory
 [codepack_snapshot]
 source = memory
 
+[callback]
+source = file
+path = scripts
+
 [apiserver]
 scheduler = self
-supervisor = self
 
 [scheduler]
 source = mongodb
@@ -35,10 +32,49 @@ supervisor = http://localhost:8000
 [worker]
 source = kafka
 topic = test
-group_id = codepack_worker
-interval = 1
+group_id = codepack_worker_test
+interval = 5
 supervisor = http://localhost:8000
+script_path = scripts/run_snapshot.py
+logger = worker-logger
 
 [supervisor]
 source = kafka
 topic = test
+
+[docker_manager]
+source = docker
+path = scripts
+run_opt = {"dns": ["8.8.8.8"]}
+
+[interpreter_manager]
+path = /home/codepack/anaconda3/envs
+run_opt = {}
+
+[logger]
+config_path = logging.json
+name = default-logger
+log_dir = logs
+
+[ssh]
+ssh_host = localhost
+ssh_port = 22
+ssh_username = test
+ssh_password = 1234
+
+[mongodb]
+host = localhost
+port = 27017
+
+[kafka]
+bootstrap_servers = localhost:9092
+
+[docker]
+base_url = unix://var/run/docker.sock
+
+[s3]
+service_name = s3
+region_name = ?
+endpoint_url = https://?
+aws_access_key_id = ?
+aws_secret_access_key = ?
diff --git a/config/test_conn.ini b/config/test_conn.ini
deleted file mode 100644
index 1221196..0000000
--- a/config/test_conn.ini
+++ /dev/null
@@ -1,6 +0,0 @@
-[mongodb]
-host = localhost
-port = 27017
-
-[kafka]
-bootstrap_servers = localhost:9092
diff --git a/tests/config/__init__.py b/tests/config/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/config/test_alias.py b/tests/config/test_alias.py
new file mode 100644
index 0000000..01dbd42
--- /dev/null
+++ b/tests/config/test_alias.py
@@ -0,0 +1,109 @@
+from codepack import Alias, StorageService
+from codepack.storage import MemoryStorage
+import os
+import pytest
+import configparser
+
+
+def test_alias_from_individual_os_env():
+    a = Alias()
+    assert a.aliases is None
+    assert isinstance(a['storage_service'], StorageService.__class__)
+    os_env = 'CODEPACK_ALIAS_STORAGE_SERVICE'
+    try:
+        os.environ[os_env] = 'codepack.storage.memory_storage.MemoryStorage'
+        assert a['storage_service'] == MemoryStorage
+    finally:
+        os.environ.pop(os_env, None)
+
+
+def test_alias_from_alias_path_os_env():
+    a = Alias()
+    assert a.aliases is None
+    assert isinstance(a['storage_service'], StorageService.__class__)
+    os_env = 'CODEPACK_ALIAS_PATH'
+    try:
+        os.environ[os_env] = 'codepack/config/default/alias.ini'
+        assert a['storage_service'] == StorageService
+    finally:
+        os.environ.pop(os_env, None)
+
+
+def test_alias_from_config_path_os_env():
+    a = Alias()
+    assert a.aliases is None
+    assert isinstance(a['storage_service'], StorageService.__class__)
+    os_env1 = 'CODEPACK_CONFIG_DIR'
+    os_env2 = 'CODEPACK_CONFIG_PATH'
+    try:
+        os.environ[os_env1] = 'config'
+        os.environ[os_env2] = 'test.ini'
+        assert a['storage_service'] == StorageService
+    finally:
+        os.environ.pop(os_env1, None)
+        os.environ.pop(os_env2, None)
+
+
+def test_alias_priority1():
+    a = Alias(data={'storage_service': 'codepack.storage.memory_storage.MemoryStorage'})
+    assert a.aliases is not None
+    assert a['storage_service'] == MemoryStorage
+    os_env1 = 'CODEPACK_ALIAS_STORAGE_SERVICE'
+    os_env2 = 'CODEPACK_ALIAS_PATH'
+    os_env3 = 'CODEPACK_CONFIG_PATH'
+    try:
+        os.environ[os_env1] = 'codepack.service.snapshot_service.SnapshotService'
+        assert a['storage_service'] == MemoryStorage
+        os.environ.pop(os_env1, None)
+        os.environ[os_env2] = 'codepack/config/default/alias.ini'
+        assert a['storage_service'] == MemoryStorage
+        os.environ.pop(os_env2, None)
+        os.environ[os_env3] = 'config/test.ini'
+        assert a['storage_service'] == MemoryStorage
+    finally:
+        os.environ.pop(os_env1, None)
+        os.environ.pop(os_env2, None)
+        os.environ.pop(os_env3, None)
+
+
+def test_alias_priority2():
+    a = Alias()
+    assert a.aliases is None
+    assert isinstance(a['storage_service'], StorageService.__class__)
+    first_os_env = 'CODEPACK_ALIAS_STORAGE_SERVICE'
+    second_os_env = 'CODEPACK_ALIAS_PATH'
+    third_os_env = 'CODEPACK_CONFIG_PATH'
+    try:
+        os.environ[first_os_env] = 'codepack.storage.memory_storage.MemoryStorage'
+        assert a['storage_service'] == MemoryStorage
+        os.environ[second_os_env] = 'codepack/config/default/alias.ini'
+        assert a['storage_service'] == MemoryStorage
+        os.environ.pop(second_os_env)
+        os.environ[third_os_env] = 'config/test.ini'
+        assert a['storage_service'] == MemoryStorage
+    finally:
+        os.environ.pop(first_os_env, None)
+        os.environ.pop(second_os_env, None)
+        os.environ.pop(third_os_env, None)
+
+
+def test_alias_path_argument():
+    os_env = 'CODEPACK_CONFIG_DIR'
+    try:
+        a = Alias(data='codepack/config/default/alias.ini')
+        assert a['storage_service'] == StorageService
+        os.environ[os_env] = 'config'
+        with pytest.raises(configparser.NoSectionError):
+            Alias(data='config/test.ini')
+    finally:
+        os.environ.pop(os_env, None)
+
+
+def test_default_alias():
+    a = Alias()
+    assert a is not None
+    assert a.aliases is None
+    default_aliases = a.get_default_alias()
+    assert isinstance(default_aliases, dict) and len(default_aliases) > 0
+    assert a['memory_storage'] == MemoryStorage
+    assert a['storage_service'] == StorageService
diff --git a/tests/config/test_config.py b/tests/config/test_config.py
new file mode 100644
index 0000000..72bbc04
--- /dev/null
+++ b/tests/config/test_config.py
@@ -0,0 +1,536 @@
+from codepack import Code, Config, Default, DeliveryService, SnapshotService, StorageService
+from codepack.storage import MemoryStorage, FileStorage, MongoStorage
+from tests import *
+from unittest.mock import patch
+import pytest
+import os
+
+
+def test_no_config():
+    code = Code(add2)
+    assert 'delivery' in code.service and isinstance(code.service['delivery'], DeliveryService)
+    assert 'snapshot' in code.service and isinstance(code.service['snapshot'], SnapshotService)
+    assert 'storage' in code.service and isinstance(code.service['storage'], StorageService)
+    assert isinstance(code.service['delivery'].storage, MemoryStorage)
+    assert isinstance(code.service['snapshot'].storage, MemoryStorage)
+    assert isinstance(code.service['storage'].storage, MemoryStorage)
+
+
+def test_some_os_env(fake_mongodb):
+    try:
+        os.environ['CODEPACK_DELIVERY_SOURCE'] = 'mongodb'
+        os.environ['CODEPACK_DELIVERY_DB'] = 'codepack'
+        os.environ['CODEPACK_DELIVERY_COLLECTION'] = 'test'
+        code = Code(add2)
+        assert 'delivery' in code.service and isinstance(code.service['delivery'], DeliveryService)
+        assert 'snapshot' in code.service and isinstance(code.service['snapshot'], SnapshotService)
+        assert 'storage' in code.service and isinstance(code.service['storage'], StorageService)
+        assert isinstance(code.service['delivery'].storage, MongoStorage)
+        assert isinstance(code.service['snapshot'].storage, MemoryStorage)
+        assert isinstance(code.service['storage'].storage, MemoryStorage)
+        assert code.service['delivery'].storage.db == 'codepack'
+        assert code.service['delivery'].storage.collection == 'test'
+    finally:
+        os.environ.pop('CODEPACK_DELIVERY_SOURCE', None)
+        os.environ.pop('CODEPACK_DELIVERY_DB', None)
+        os.environ.pop('CODEPACK_DELIVERY_COLLECTION', None)
+
+
+def test_default_config_with_os_env(default_os_env):
+    code = Code(add2)
+    assert code(1, 2) == 3
+
+
+def test_config_path():
+    code = Code(add2, config_path='config/test.ini')
+    ret = code(1, 2)
+    assert ret == 3
+
+
+def test_config_path_priority():
+    try:
+        config = Config()
+        _config = config.get_config(section='logger', ignore_error=True)
+        assert _config == {'log_dir': 'logs', 'name': 'default-logger',
+                           'config_path': os.path.join(config.get_default_config_dir(), 'logging.json')}
+        with pytest.raises(AssertionError):
+            Config(config_path='test.ini')
+        os.environ['CODEPACK_CONFIG_DIR'] = 'config'
+        config = Config(config_path='test.ini')
+        assert config.config_path == os.path.join('config', 'test.ini')
+        _config = config.get_config(section='logger')
+        ref = {'name': 'default-logger', 'config_path': 'logging.json', 'log_dir': 'logs'}
+        assert _config == ref
+        os.environ['CODEPACK_CONFIG_PATH'] = 'codepack.ini'
+        _config = config.get_config(section='logger')
+        assert _config == ref
+        _config = config.get_config(section='logger', config_path='test.ini')
+        assert _config == ref
+    finally:
+        os.environ.pop('CODEPACK_CONFIG_PATH', None)
+        os.environ.pop('CODEPACK_CONFIG_DIR', None)
+
+
+def test_config_get_value_priority():
+    try:
+        config = Config()
+        _config = config.get_config(section='logger')
+        assert _config == {'name': 'default-logger', 'log_dir': 'logs',
+                           'config_path': os.path.join(config.get_default_config_dir(), 'logging.json')}
+        default_value = Config.collect_value(section='logger', key='name', config=dict())
+        assert default_value == 'default-logger'
+        name = Config.collect_value(section='logger', key='name', config=_config)
+        assert name == 'default-logger'
+        os.environ['CODEPACK_LOGGER_NAME'] = 'test-logger'
+        name = Config.collect_value(section='logger', key='name', config=_config)
+        assert name == 'test-logger'
+        os.environ.pop('CODEPACK_LOGGER_NAME')
+        os.environ['CODEPACK_CONFIG_PATH'] = 'config/test.ini'
+        os.environ['CODEPACK_CONFIG_DIR'] = 'config'
+        config = Config()
+        _config = config.get_config(section='logger')
+        assert _config == {'name': 'default-logger',
+                           'config_path': os.path.join('config', 'logging.json'),
+                           'log_dir': 'logs'}
+        name = Config.collect_value(section='logger', key='name', config=_config)
+        assert name == 'default-logger'
+        os.environ['CODEPACK_LOGGER_NAME'] = 'test-logger'
+        name = Config.collect_value(section='logger', key='name', config=_config)
+        assert name == 'test-logger'
+    finally:
+        os.environ.pop('CODEPACK_LOGGER_NAME', None)
+        os.environ.pop('CODEPACK_CONFIG_PATH', None)
+        os.environ.pop('CODEPACK_CONFIG_DIR', None)
+
+
+def test_get_storage_config_priority():
+    try:
+        os.environ['CODEPACK_CONFIG_DIR'] = 'config'
+        config = Config('test.ini')
+        storage_config = config.get_storage_config('worker')
+        ref = {'group_id': 'codepack_worker_test', 'interval': '5', 'kafka': {'bootstrap_servers': 'localhost:9092'},
+               'script_path': 'scripts/run_snapshot.py', 'source': 'kafka', 'supervisor': 'http://localhost:8000',
+               'topic': 'test', 'logger': 'worker-logger'}
+        assert storage_config == ref
+        os.environ['CODEPACK_WORKER_SCRIPT'] = 'test_script.py'
+        os.environ['CODEPACK_WORKER_TOPIC'] = 'test2'
+        storage_config = config.get_storage_config('worker')
+        assert storage_config == ref
+        os.environ['CODEPACK_WORKER_TEST_KEY'] = 'test'
+        storage_config = config.get_storage_config('worker')
+        assert storage_config == ref
+    finally:
+        os.environ.pop('CODEPACK_CONFIG_DIR', None)
+        os.environ.pop('CODEPACK_WORKER_SCRIPT', None)
+        os.environ.pop('CODEPACK_WORKER_TOPIC', None)
+        os.environ.pop('CODEPACK_WORKER_TEST_KEY', None)
+
+
+def test_default_memory_code_snapshot_service_with_os_env(fake_mongodb):
+    mss = None
+    try:
+        config = Config()
+        storage_config = config.get_storage_config('code_snapshot')
+        assert storage_config == {'source': 'memory'}
+        os.environ['CODEPACK_CODESNAPSHOT_SOURCE'] = 'mongodb'
+        os.environ['CODEPACK_CODESNAPSHOT_DB'] = 'test_db'
+        os.environ['CODEPACK_CODESNAPSHOT_COLLECTION'] = 'test_collection'
+        mss = Default.get_service('code_snapshot', 'snapshot_service')
+        assert hasattr(mss.storage, 'mongodb')
+        assert mss.storage.db == 'test_db'
+        assert mss.storage.collection == 'test_collection'
+    finally:
+        if mss is not None and not mss.storage.mongodb.closed():
+            mss.storage.mongodb.close()
+        os.environ.pop('CODEPACK_CODESNAPSHOT_SOURCE', None)
+        os.environ.pop('CODEPACK_CODESNAPSHOT_DB', None)
+        os.environ.pop('CODEPACK_CODESNAPSHOT_COLLECTION', None)
+
+
+def test_default_file_code_snapshot_service_with_os_env():
+    config = Config()
+    storage_config = config.get_storage_config('code_snapshot')
+    assert storage_config == {'source': 'memory'}
+    env_source = 'CODEPACK_CODESNAPSHOT_SOURCE'
+    env_path = 'CODEPACK_CODESNAPSHOT_PATH'
+    try:
+        os.environ[env_source] = 'file'
+        os.environ[env_path] = 'tmp/'
+        fss = Default.get_service('code_snapshot', 'snapshot_service')
+        assert hasattr(fss.storage, 'path')
+        assert fss.storage.path == 'tmp/'
+    finally:
+        for env in [env_source, env_path]:
+            os.environ.pop(env, None)
+
+
+def test_default_mongo_code_snapshot_service_with_os_env():
+    config = Config()
+    storage_config = config.get_storage_config('code_snapshot')
+    assert storage_config == {'source': 'memory'}
+    env_source = 'CODEPACK_CODESNAPSHOT_SOURCE'
+    env_db = 'CODEPACK_CODESNAPSHOT_DB'
+    env_collection = 'CODEPACK_CODESNAPSHOT_COLLECTION'
+    mss = None
+    try:
+        os.environ[env_source] = 'mongodb'
+        os.environ[env_db] = 'test'
+        os.environ[env_collection] = 'snapshot'
+        mss = Default.get_service('code_snapshot', 'snapshot_service')
+    finally:
+        if mss is not None and not mss.storage.mongodb.closed():
+            mss.storage.mongodb.close()
+        for env in [env_source, env_db, env_collection]:
+            os.environ.pop(env, None)
+    assert hasattr(mss.storage, 'mongodb')
+
+
+def test_default_memory_delivery_service_with_os_env():
+    config = Config()
+    storage_config = config.get_storage_config('delivery')
+    assert storage_config == {'source': 'memory'}
+    env_source = 'CODEPACK_DELIVERY_SOURCE'
+    try:
+        os.environ[env_source] = 'memory'
+        mds = Default.get_service('delivery', 'delivery_service')
+        assert isinstance(mds.storage, MemoryStorage)
+    finally:
+        for env in [env_source]:
+            os.environ.pop(env, None)
+
+
+def test_default_file_delivery_service_with_os_env():
+    config = Config()
+    storage_config = config.get_storage_config('delivery')
+    assert storage_config == {'source': 'memory'}
+    env_source = 'CODEPACK_DELIVERY_SOURCE'
+    env_path = 'CODEPACK_DELIVERY_PATH'
+    try:
+        os.environ[env_source] = 'file'
+        os.environ[env_path] = 'tmp/'
+        fds = Default.get_service('delivery', 'delivery_service')
+        assert hasattr(fds.storage, 'path')
+        assert fds.storage.path == 'tmp/'
+    finally:
+        for env in [env_source, env_path]:
+            os.environ.pop(env, None)
+
+
+def test_default_mongo_delivery_service_with_os_env():
+    config = Config()
+    storage_config = config.get_storage_config('delivery')
+    assert storage_config == {'source': 'memory'}
+    env_source = 'CODEPACK_DELIVERY_SOURCE'
+    env_db = 'CODEPACK_DELIVERY_DB'
+    env_collection = 'CODEPACK_DELIVERY_COLLECTION'
+    mds = None
+    try:
+        os.environ[env_source] = 'mongodb'
+        os.environ[env_db] = 'test'
+        os.environ[env_collection] = 'delivery'
+        mds = Default.get_service('delivery', 'delivery_service')
+        assert hasattr(mds.storage, 'mongodb')
+        assert mds.storage.db == 'test'
+        assert mds.storage.collection == 'delivery'
+    finally:
+        for env in [env_source, env_db, env_collection]:
+            os.environ.pop(env, None)
+        if mds is not None and not mds.storage.mongodb.closed():
+            mds.storage.mongodb.close()
+
+
+def test_default_memory_code_storage_service_with_os_env():
+    config = Config()
+    storage_config = config.get_storage_config('code')
+    assert storage_config == {'source': 'memory'}
+    env_source = 'CODEPACK_CODE_SOURCE'
+    try:
+        os.environ[env_source] = 'memory'
+        mss = Default.get_service('code', 'storage_service')
+        assert hasattr(mss.storage, 'memory')
+        assert mss.storage.item_type == Code
+    finally:
+        if env_source in os.environ:
+            os.environ.pop(env_source, None)
+
+
+def test_default_file_code_storage_service_with_os_env():
+    config = Config()
+    storage_config = config.get_storage_config('code')
+    assert storage_config == {'source': 'memory'}
+    env_source = 'CODEPACK_CODE_SOURCE'
+    env_path = 'CODEPACK_CODE_PATH'
+    try:
+        os.environ[env_source] = 'file'
+        os.environ[env_path] = 'tmp/'
+        fss = Default.get_service('code', 'storage_service')
+        assert hasattr(fss.storage, 'path')
+        assert fss.storage.path == 'tmp/'
+        assert fss.storage.item_type == Code
+    finally:
+        for env in [env_source, env_path]:
+            if env in os.environ:
+                os.environ.pop(env, None)
+
+
+def test_default_mongo_code_storage_service_with_os_env():
+    config = Config()
+    storage_config = config.get_storage_config('code')
+    assert storage_config == {'source': 'memory'}
+    env_source = 'CODEPACK_CODE_SOURCE'
+    env_db = 'CODEPACK_CODE_DB'
+    env_collection = 'CODEPACK_CODE_COLLECTION'
+    mss = None
+    try:
+        os.environ[env_source] = 'mongodb'
+        os.environ[env_db] = 'test'
+        os.environ[env_collection] = 'codes'
+        mss = Default.get_service('code', 'storage_service')
+        assert hasattr(mss.storage, 'mongodb')
+        assert mss.storage.item_type == Code
+        assert mss.storage.db == 'test'
+        assert mss.storage.collection == 'codes'
+    finally:
+        for env in [env_source, env_db, env_collection]:
+            os.environ.pop(env, None)
+        if mss is not None and not mss.storage.mongodb.closed():
+            mss.storage.mongodb.close()
+
+
+def test_if_default_services_have_single_instance_for_each_service(testdir_snapshot_service):
+    os.environ['CODEPACK_DELIVERY_SOURCE'] = 'memory'
+    os.environ['CODEPACK_CODESNAPSHOT_SOURCE'] = 'file'
+    os.environ['CODEPACK_CODESNAPSHOT_PATH'] = testdir_snapshot_service
+    os.environ['CODEPACK_CODE_SOURCE'] = 'mongodb'
+    os.environ['CODEPACK_CODE_DB'] = 'test'
+    os.environ['CODEPACK_CODE_COLLECTION'] = 'codes'
+    try:
+        code1 = Code(add2)
+        code2 = Code(add3)
+        assert isinstance(code1.service['delivery'], DeliveryService)
+        assert isinstance(code1.service['snapshot'], SnapshotService)
+        assert isinstance(code1.service['storage'], StorageService)
+        assert isinstance(code2.service['delivery'], DeliveryService)
+        assert isinstance(code2.service['snapshot'], SnapshotService)
+        assert isinstance(code2.service['storage'], StorageService)
+
+        assert isinstance(code1.service['delivery'].storage, MemoryStorage)
+        assert isinstance(code1.service['snapshot'].storage, FileStorage)
+        assert isinstance(code1.service['storage'].storage, MongoStorage)
+        assert isinstance(code2.service['delivery'].storage, MemoryStorage)
+        assert isinstance(code2.service['snapshot'].storage, FileStorage)
+        assert isinstance(code2.service['storage'].storage, MongoStorage)
+
+        assert (code1.service['delivery']) == (code2.service['delivery'])
+        assert (code1.service['snapshot']) == (code2.service['snapshot'])
+        assert (code1.service['storage']) == (code2.service['storage'])
+
+        assert id(code1.service['delivery']) == id(code2.service['delivery'])
+        assert id(code1.service['snapshot']) == id(code2.service['snapshot'])
+        assert id(code1.service['storage']) == id(code2.service['storage'])
+    finally:
+        for env in ['CODEPACK_DELIVERY_SOURCE',
+                    'CODEPACK_CODESNAPSHOT_SOURCE', 'CODEPACK_CODESNAPSHOT_PATH',
+                    'CODEPACK_CODE_SOURCE', 'CODEPACK_CODE_DB', 'CODEPACK_CODE_COLLECTION']:
+            os.environ.pop(env, None)
+
+
+def test_config_dir():
+    path = Config.collect_value(section='?', key='path', config={'path': 'config/test.ini'})
+    assert path == 'config/test.ini'
+    with pytest.raises(AssertionError):
+        Config.collect_value(section='conn', key='path', config={'path': 'test.ini'})
+    try:
+        os.environ['CODEPACK_CONFIG_DIR'] = 'config'
+        path = Config.collect_value(section='conn', key='path', config={'path': 'test.ini'})
+        assert path == os.path.join('config', 'test.ini')
+    finally:
+        os.environ.pop('CODEPACK_CONFIG_DIR', None)
+
+
+@patch('codepack.config.config.Config.parse_config')
+def test_get_config_without_anything(parse_config):
+    config = Config()
+    ret = config.get_config('worker')
+    assert ret is not None
+    arg_list = parse_config.call_args_list
+    assert len(arg_list) == 2
+    args, kwargs = arg_list[0]
+    assert kwargs == {'section': 'worker', 'config_path': config.get_default_config_path()}
+    args, kwargs = arg_list[1]
+    assert kwargs == {'section': 'worker',
+                      'config_path': config.get_default_config_path()}
+
+
+@patch('codepack.config.config.Config.parse_config')
+def test_get_config_without_anything_but_os_env(parse_config):
+    os.environ['CODEPACK_CONFIG_PATH'] = 'config/test.ini'
+    try:
+        config = Config()
+        ret = config.get_config('worker')
+        assert ret is not None
+        arg_list = parse_config.call_args_list
+        assert len(arg_list) == 2
+        args, kwargs = arg_list[0]
+        assert kwargs == {'section': 'worker', 'config_path': 'config/test.ini'}
+        args, kwargs = arg_list[1]
+        assert kwargs == {'section': 'worker',
+                          'config_path': config.get_default_config_path()}
+    finally:
+        os.environ.pop('CODEPACK_CONFIG_PATH')
+
+
+@patch('codepack.config.config.Config.parse_config')
+def test_get_config_without_anything_but_method_argument(parse_config):
+    config = Config()
+    ret = config.get_config('worker', config_path='config/test.ini')
+    assert ret is not None
+    parse_config.assert_called_once_with(section='worker', config_path='config/test.ini')
+
+
+@patch('codepack.config.config.Config.parse_config')
+def test_get_config_with_constructor_argument(parse_config):
+    config = Config(config_path='config/test.ini')
+    ret = config.get_config('worker')
+    assert ret is not None
+    parse_config.assert_called_once_with(section='worker', config_path='config/test.ini')
+
+
+@patch('codepack.config.config.Config.parse_config')
+def test_get_config_with_constructor_argument_and_os_env(parse_config):
+    try:
+        os.environ['CODEPACK_CONFIG_PATH'] = 'config/codepack.ini'
+        config = Config(config_path='config/test.ini')
+        ret = config.get_config('worker')
+        assert ret is not None
+        parse_config.assert_called_once_with(section='worker', config_path='config/test.ini')
+    finally:
+        os.environ.pop('CODEPACK_CONFIG_PATH')
+
+
+@patch('codepack.config.config.Config.parse_config')
+def test_get_config_with_constructor_argument_and_method_argument(parse_config):
+    config = Config(config_path='config/test.ini')
+    ret = config.get_config('worker', config_path='config/codepack.ini')
+    assert ret is not None
+    parse_config.assert_called_once_with(section='worker', config_path='config/codepack.ini')
+
+
+@patch('codepack.config.config.Config.parse_config')
+def test_get_config_with_method_argument_and_os_env(parse_config):
+    try:
+        os.environ['CODEPACK_CONFIG_PATH'] = 'config/codepack.ini'
+        config = Config()
+        ret = config.get_config('worker', config_path='config/test.ini')
+        assert ret is not None
+        parse_config.assert_called_once_with(section='worker', config_path='config/test.ini')
+    finally:
+        os.environ.pop('CODEPACK_CONFIG_PATH')
+
+
+def test_collect_values_without_anything():
+    os_envs = {'CODEPACK_WORKER_LOGGER': 'dummy-logger',
+               'CODEPACK_WORKER_DUMMY': 'dummy_value'}
+    try:
+        for k, v in os_envs.items():
+            os.environ[k] = v
+        config = Config()
+        ret = config.get_config('worker')
+        assert ret == {'dummy': 'dummy_value',
+                       'background': 'True',
+                       'interval': '1',
+                       'logger': 'dummy-logger',
+                       'source': 'memory',
+                       'topic': 'codepack',
+                       'script_path': os.path.join(config.get_default_config_dir(), 'scripts/run_snapshot.py')}
+    finally:
+        for k in os_envs.keys():
+            os.environ.pop(k, None)
+
+
+def test_collect_values_without_anything_but_os_env():
+    os_envs = {'CODEPACK_CONFIG_PATH': 'config/test.ini',
+               'CODEPACK_WORKER_LOGGER': 'dummy-logger',
+               'CODEPACK_WORKER_DUMMY': 'dummy_value'}
+    try:
+        for k, v in os_envs.items():
+            os.environ[k] = v
+        config = Config()
+        ret = config.get_config('worker')
+        assert ret == {'background': 'True', 'dummy': 'dummy_value',
+                       'group_id': 'codepack_worker_test',
+                       'interval': '5',
+                       'logger': 'dummy-logger',
+                       'script_path': 'scripts/run_snapshot.py',
+                       'source': 'kafka',
+                       'supervisor': 'http://localhost:8000',
+                       'topic': 'test'}
+    finally:
+        for k in os_envs.keys():
+            os.environ.pop(k, None)
+
+
+def test_collect_values_without_anything_but_method_argument():
+    os_envs = {'CODEPACK_WORKER_LOGGER': 'dummy-logger',
+               'CODEPACK_WORKER_DUMMY': 'dummy_value'}
+    try:
+        for k, v in os_envs.items():
+            os.environ[k] = v
+        config = Config()
+        ret = config.get_config('worker', config_path='config/test.ini')
+        assert ret == {'group_id': 'codepack_worker_test',
+                       'interval': '5',
+                       'logger': 'worker-logger',
+                       'script_path': 'scripts/run_snapshot.py',
+                       'source': 'kafka',
+                       'supervisor': 'http://localhost:8000',
+                       'topic': 'test'}
+    finally:
+        for k in os_envs.keys():
+            os.environ.pop(k, None)
+
+
+def test_collect_values_with_constructor_argument():
+    os_envs = {'CODEPACK_WORKER_LOGGER': 'dummy-logger',
+               'CODEPACK_WORKER_DUMMY': 'dummy_value'}
+    try:
+        for k, v in os_envs.items():
+            os.environ[k] = v
+        config = Config(config_path='config/test.ini')
+        ret = config.get_config('worker')
+        assert ret == {'group_id': 'codepack_worker_test',
+                       'interval': '5',
+                       'logger': 'worker-logger',
+                       'script_path': 'scripts/run_snapshot.py',
+                       'source': 'kafka',
+                       'supervisor': 'http://localhost:8000',
+                       'topic': 'test'}
+    finally:
+        for k in os_envs.keys():
+            os.environ.pop(k, None)
+
+
+def test_get_storage_config_with_os_env():
+    try:
+        os.environ['CODEPACK_CONFIG_PATH'] = 'config/test.ini'
+        config = Config()
+        ret = config.get_storage_config('scheduler')
+        ref = {'collection': 'scheduler', 'db': 'codepack', 'source': 'mongodb',
+               'supervisor': 'http://localhost:8000',
+               'mongodb': {'host': 'localhost', 'port': '27017'}}
+        assert ret == ref
+        os.environ['CODEPACK_SCHEDULER_DB'] = 'test'
+        ret = config.get_storage_config('scheduler')
+        ref['db'] = 'test'
+        assert ret == ref
+        os.environ['CODEPACK_MONGODB_HOST'] = '127.0.0.1'
+        os.environ['CODEPACK_MONGODB_USER'] = 'admin'
+        ret = config.get_storage_config('scheduler')
+        ref['mongodb']['host'] = '127.0.0.1'
+        ref['mongodb']['user'] = 'admin'
+        assert ret == ref
+    finally:
+        os.environ.pop('CODEPACK_CONFIG_PATH', None)
+        os.environ.pop('CODEPACK_SCHEDULER_DB', None)
+        os.environ.pop('CODEPACK_MONGODB_HOST', None)
+        os.environ.pop('CODEPACK_MONGODB_USER', None)
diff --git a/tests/config/test_default.py b/tests/config/test_default.py
new file mode 100644
index 0000000..c203b23
--- /dev/null
+++ b/tests/config/test_default.py
@@ -0,0 +1,282 @@
+from codepack.storage import MemoryStorage, FileStorage, MongoStorage, MemoryJobStore, MongoJobStore, MemoryMessenger
+from codepack import DeliveryService, CallbackService, SnapshotService,\
+    Scheduler, Worker, Supervisor, DockerManager, InterpreterManager, Default
+from unittest.mock import patch
+from collections.abc import Callable
+import os
+import inspect
+import logging
+
+
+def test_default_init_with_nothing():
+    Default()
+    assert len(Default.instances) == 0
+    assert Default.config is not None
+    assert Default.config.config_path is None
+    assert Default.alias is not None
+    assert Default.alias.aliases is None
+
+
+def test_default_init_with_os_env():
+    try:
+        os.environ['CODEPACK_CONFIG_DIR'] = 'config'
+        os.environ['CODEPACK_CONFIG_PATH'] = 'test.ini'
+        Default()
+        assert len(Default.instances) == 0
+        assert Default.config is not None
+        assert Default.config.config_path is None
+        assert Default.alias is not None
+        assert Default.alias.aliases is None
+    finally:
+        os.environ.pop('CODEPACK_CONFIG_DIR', None)
+        os.environ.pop('CODEPACK_CONFIG_PATH', None)
+
+
+def test_default_init_with_config_path():
+    Default(config_path='config/test.ini')
+    assert len(Default.instances) == 0
+    assert Default.config is not None
+    assert Default.config.config_path == 'config/test.ini'
+    assert Default.alias is not None
+    assert Default.alias.aliases is None
+
+
+def test_default_init_with_config_path_and_alias_path_os_env():
+    try:
+        os.environ['CODEPACK_ALIAS_PATH'] = 'codepack/config/default/alias.ini'
+        Default(config_path='config/test.ini')
+        assert len(Default.instances) == 0
+        assert Default.config is not None
+        assert Default.config.config_path == 'config/test.ini'
+        assert Default.alias is not None
+        assert Default.alias.aliases is None
+    finally:
+        os.environ.pop('CODEPACK_ALIAS_PATH', None)
+
+
+def test_default_init_with_alias_path():
+    Default(alias_path='codepack/config/default/alias.ini')
+    assert len(Default.instances) == 0
+    assert Default.config is not None
+    assert Default.config.config_path is None
+    assert Default.alias is not None
+    assert Default.alias.aliases is not None and isinstance(Default.alias.aliases, dict)
+
+
+def test_default_get_storage_config():
+    Default(config_path='config/test.ini')
+    assert len(Default.instances) == 0
+    config = Default.get_config_instance()
+    storage_config = config.get_storage_config('worker')
+    assert storage_config == {'source': 'kafka', 'topic': 'test', 'kafka': {'bootstrap_servers': 'localhost:9092'},
+                              'group_id': 'codepack_worker_test', 'interval': '5',
+                              'supervisor': 'http://localhost:8000', 'script_path': 'scripts/run_snapshot.py',
+                              'logger': 'worker-logger'}
+
+
+def test_default_get_alias_from_source():
+    assert Default.get_alias_from_source(source='memory') == 'memory'
+    assert Default.get_alias_from_source(source='memory', prefix='front') == 'front_memory'
+    assert Default.get_alias_from_source(source='memory', suffix='back') == 'memory_back'
+    assert Default.get_alias_from_source(source='memory', prefix='front', suffix='back') == 'front_memory_back'
+    assert Default.get_alias_from_source(source='mongodb') == 'mongo'
+    assert Default.get_alias_from_source(source='mongodb', prefix='front') == 'front_mongo'
+    assert Default.get_alias_from_source(source='mongodb', suffix='back') == 'mongo_back'
+    assert Default.get_alias_from_source(source='mongodb', prefix='front', suffix='back') == 'front_mongo_back'
+
+
+def test_default_get_class_from_alias():
+    try:
+        Default.config = None
+        Default.alias = None
+        Default.instances = dict()
+        isinstance(Default.get_class_from_alias(alias='memory_storage'), MemoryStorage.__class__)
+        os.environ['CODEPACK_ALIAS_MEMORY_STORAGE'] = 'codepack.storage.file_storage.FileStorage'
+        memory_storage = Default.get_class_from_alias(alias='file_storage')
+        assert isinstance(memory_storage, MemoryStorage.__class__)
+        os.environ.pop('CODEPACK_ALIAS_MEMORY_STORAGE', None)
+        Default.config = None
+        Default.alias = None
+        os.environ['CODEPACK_ALIAS_PATH'] = 'codepack/config/default/alias.ini'
+        memory_storage = Default.get_class_from_alias(alias='memory_storage')
+        assert isinstance(memory_storage, MemoryStorage.__class__)
+        os.environ.pop('CODEPACK_ALIAS_PATH', None)
+        Default.config = None
+        Default.alias = None
+        memory_storage = Default.get_class_from_alias(alias='memory_storage',
+                                                      alias_path='codepack/config/default/alias.ini')
+        assert isinstance(memory_storage, MemoryStorage.__class__)
+        Default.config = None
+        Default.alias = None
+        os.environ['CODEPACK_CONFIG_DIR'] = 'config'
+        os.environ['CODEPACK_CONFIG_PATH'] = 'config/test.ini'
+        memory_storage = Default.get_class_from_alias(alias='memory_storage')
+        assert isinstance(memory_storage, MemoryStorage.__class__)
+    finally:
+        os.environ.pop('CODEPACK_ALIAS_MEMORY_STORAGE', None)
+        os.environ.pop('CODEPACK_ALIAS_PATH', None)
+        os.environ.pop('CODEPACK_CONFIG_DIR', None)
+        os.environ.pop('CODEPACK_CONFIG_PATH', None)
+
+
+def test_get_default_service():
+    service = Default.get_service('delivery', 'delivery_service')
+    assert isinstance(service, DeliveryService) and isinstance(service.storage, MemoryStorage)
+
+
+def test_get_default_scheduler():
+    scheduler = Default.get_scheduler()
+    assert isinstance(scheduler, Scheduler)
+    assert 'codepack' in scheduler.jobstores
+    jobstore = scheduler.jobstores['codepack']
+    assert isinstance(jobstore, MemoryJobStore)
+    assert scheduler.supervisor is None
+
+
+@patch('pymongo.MongoClient')
+def test_get_default_scheduler_with_some_os_env(mock_client):
+    try:
+        os.environ['CODEPACK_SCHEDULER_SOURCE'] = 'mongodb'
+        os.environ['CODEPACK_SCHEDULER_DB'] = 'test_db'
+        os.environ['CODEPACK_SCHEDULER_COLLECTION'] = 'test_collection'
+        os.environ['CODEPACK_SCHEDULER_SUPERVISOR'] = 'dummy_supervisor'
+        os.environ['CODEPACK_MONGODB_REPLICASET'] = 'TEST'
+        scheduler = Default.get_scheduler()
+        assert isinstance(scheduler, Scheduler)
+        assert 'codepack' in scheduler.jobstores
+        jobstore = scheduler.jobstores['codepack']
+        assert isinstance(jobstore, MongoJobStore)
+        assert scheduler.supervisor == 'dummy_supervisor'
+        mock_client.assert_called_once_with(host='localhost', port=27017, replicaset='TEST')
+        mock_client().__getitem__.assert_called_once_with('test_db')
+        mock_client().__getitem__().__getitem__.assert_called_once_with('test_collection')
+    finally:
+        os.environ.pop('CODEPACK_SCHEDULER_SOURCE', None)
+        os.environ.pop('CODEPACK_SCHEDULER_DB', None)
+        os.environ.pop('CODEPACK_SCHEDULER_COLLECTION', None)
+        os.environ.pop('CODEPACK_SCHEDULER_SUPERVISOR', None)
+        os.environ.pop('CODEPACK_MONGODB_REPLICASET', None)
+
+
+@patch('docker.DockerClient')
+def test_get_default_worker_without_os_env(mock_docker_client):
+    worker = Default.get_employee('worker')
+    assert isinstance(worker, Worker)
+    assert isinstance(worker.messenger, MemoryMessenger)
+    assert worker.messenger.topic == 'codepack'
+    assert worker.supervisor is None
+    mock_docker_client.assert_called_once_with(base_url='unix://var/run/docker.sock')
+    assert worker.docker_manager.docker.session == mock_docker_client()
+    assert isinstance(worker.callback_service, CallbackService)
+    assert isinstance(worker.callback_service.storage, FileStorage)
+    default_dir = Default.get_config_instance().get_default_config_dir()
+    assert worker.callback_service.storage.path == os.path.join(default_dir, 'scripts')
+    assert isinstance(worker.logger, logging.Logger)
+    assert worker.logger.name == 'worker-logger'
+    assert hasattr(worker, 'script') and worker.script == 'run_snapshot.py'
+    assert hasattr(worker, 'script_dir') and worker.script_dir == os.path.join(default_dir, 'scripts')
+    assert hasattr(worker, 'script_path') and worker.script_path == os.path.join(default_dir, 'scripts/run_snapshot.py')
+
+
+@patch('docker.DockerClient')
+@patch('kafka.KafkaConsumer')
+def test_get_default_worker_with_os_env(mock_kafka_consumer, mock_docker_client):
+    try:
+        os.environ['CODEPACK_CONFIG_DIR'] = 'config'
+        os.environ['CODEPACK_CONFIG_PATH'] = 'codepack.ini'
+        worker = Default.get_employee('worker')
+        assert isinstance(worker, Worker)
+        arg_list = mock_kafka_consumer.call_args_list
+        assert len(arg_list) == 1
+        args, kwargs = arg_list[0]
+        assert kwargs.get('bootstrap_servers', '') == '?:9092,?:9092,?:9092'
+        assert isinstance(kwargs.get('value_deserializer', ''), Callable)
+        assert len(args) == 1 and args[0] == 'codepack'
+        assert hasattr(worker.messenger, 'consumer')
+        assert getattr(worker.messenger, 'consumer').session is mock_kafka_consumer()
+        mock_docker_client.assert_called_once_with(base_url='unix://var/run/docker.sock')
+        assert worker.docker_manager.docker.session == mock_docker_client()
+        assert isinstance(worker.callback_service, CallbackService)
+        assert isinstance(worker.logger, logging.Logger)
+        assert worker.logger.name == 'worker-logger'
+        assert hasattr(worker, 'script') and worker.script == 'run_snapshot.py'
+        default_dir = Default.get_config_instance().get_default_config_dir()
+        script_dir = os.path.join(default_dir, 'scripts')
+        assert hasattr(worker, 'script_dir') and worker.script_dir == script_dir
+        assert hasattr(worker, 'script_path') and worker.script_path == os.path.join(default_dir,
+                                                                                     'scripts/run_snapshot.py')
+    finally:
+        os.environ.pop('CODEPACK_CONFIG_DIR', None)
+        os.environ.pop('CODEPACK_CONFIG_PATH', None)
+
+
+def test_get_default_supervisor():
+    try:
+        os.environ['CODEPACK_CODESNAPSHOT_SOURCE'] = 'mongodb'
+        os.environ['CODEPACK_CODESNAPSHOT_DB'] = 'test_db'
+        os.environ['CODEPACK_CODESNAPSHOT_COLLECTION'] = 'test_collection'
+        supervisor = Default.get_employee('supervisor')
+        assert isinstance(supervisor, Supervisor)
+        assert isinstance(supervisor.messenger, MemoryMessenger)
+        assert supervisor.messenger.topic == 'codepack'
+        assert isinstance(supervisor.snapshot_service, SnapshotService)
+        assert isinstance(supervisor.snapshot_service.storage, MongoStorage)
+    finally:
+        os.environ.pop('CODEPACK_CODESNAPSHOT_SOURCE', None)
+        os.environ.pop('CODEPACK_CODESNAPSHOT_DB', None)
+        os.environ.pop('CODEPACK_CODESNAPSHOT_COLLECTION', None)
+
+
+@patch('kafka.KafkaProducer')
+def test_get_default_supervisor_with_os_env(mock_kafka_producer):
+    try:
+        os.environ['CODEPACK_CONFIG_PATH'] = 'config/test.ini'
+        os.environ['CODEPACK_CODESNAPSHOT_SOURCE'] = 'mongodb'
+        os.environ['CODEPACK_CODESNAPSHOT_DB'] = 'test_db'
+        os.environ['CODEPACK_CODESNAPSHOT_COLLECTION'] = 'test_collection'
+        supervisor = Default.get_employee('supervisor')
+        assert isinstance(supervisor, Supervisor)
+        arg_list = mock_kafka_producer.call_args_list
+        assert len(arg_list) == 1
+        args, kwargs = arg_list[0]
+        assert kwargs.get('bootstrap_servers', '') == 'localhost:9092'
+        assert isinstance(kwargs.get('value_serializer', ''), Callable)
+        assert len(args) == 0
+        assert hasattr(supervisor.messenger, 'producer')
+        assert getattr(supervisor.messenger, 'producer').session is mock_kafka_producer()
+        assert isinstance(supervisor.snapshot_service, SnapshotService)
+        assert isinstance(supervisor.snapshot_service.storage, MongoStorage)
+    finally:
+        os.environ.pop('CODEPACK_CONFIG_PATH', None)
+        os.environ.pop('CODEPACK_CODESNAPSHOT_SOURCE', None)
+        os.environ.pop('CODEPACK_CODESNAPSHOT_DB', None)
+        os.environ.pop('CODEPACK_CODESNAPSHOT_COLLECTION', None)
+
+
+@patch('docker.DockerClient')
+def test_get_default_docker_manager(mock_client):
+    docker_manager = Default.get_docker_manager()
+    mock_client.assert_called_once_with(base_url='unix://var/run/docker.sock')
+    assert isinstance(docker_manager, DockerManager)
+    default_dir = Default.get_config_instance().get_default_config_dir()
+    assert docker_manager.path == os.path.join(default_dir, 'scripts')
+    assert docker_manager.run_opt == {'dns': ['8.8.8.8']}
+
+
+def test_get_default_interpreter_manager():
+    interpreter_manager = Default.get_interpreter_manager()
+    assert isinstance(interpreter_manager, InterpreterManager)
+    assert interpreter_manager.path == '${HOME}/anaconda3/envs'
+    assert interpreter_manager.run_opt == {}
+
+
+def test_get_default_logger():
+    logger = Default.get_logger()
+    assert isinstance(logger, logging.Logger)
+    assert logger.name == 'default-logger'
+    assert len(logger.handlers) == 1
+    assert logger.handlers[0].get_name() == 'console'
+    logger2 = Default.get_logger('worker-logger')
+    assert isinstance(logger2, logging.Logger)
+    assert logger2.name == 'worker-logger'
+    assert len(logger2.handlers) == 2
diff --git a/tests/conftest.py b/tests/conftest.py
index 5a8bcb7..e5d188a 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,8 +1,11 @@
-import pytest
+from codepack import Default, Delivery
 from codepack.interface import MongoDB
+from codepack.storage import MemoryMessenger
+import pytest
 import os
 import mongomock
-from codepack.config import Default
+
+
 from shutil import rmtree
 from glob import glob
 
@@ -29,12 +32,14 @@ def empty_dir(directory):
 
 @pytest.fixture(scope='function', autouse=False)
 def default_os_env():
-    os.environ['CODEPACK_CONFIG_PATH'] = 'config/test.ini'
-    Default.get_storage_instance('delivery', 'delivery_service').init()
-    Default.get_storage_instance('code_snapshot', 'snapshot_service').init()
-    Default.get_storage_instance('code', 'storage_service').init()
-    Default.get_storage_instance('codepack', 'storage_service').init()
+    os.environ['CODEPACK_CONFIG_DIR'] = 'config'
+    os.environ['CODEPACK_CONFIG_PATH'] = 'test.ini'
+    Default.get_service('delivery', 'delivery_service').storage.init()
+    Default.get_service('code_snapshot', 'snapshot_service').storage.init()
+    Default.get_service('code', 'storage_service').storage.init()
+    Default.get_service('codepack', 'storage_service').storage.init()
     yield
+    os.environ.pop('CODEPACK_CONFIG_DIR', None)
     os.environ.pop('CODEPACK_CONFIG_PATH', None)
 
 
@@ -91,15 +96,25 @@ def testdir_docker_manager():
     empty_dir(directory)
 
 
+@pytest.fixture(scope='function', autouse=False)
+def testdir_file_storage():
+    directory = 'testdir/file_storage/'
+    empty_dir(directory)
+    yield directory
+    empty_dir(directory)
+
+
 @pytest.fixture(scope='function', autouse=True)
 def init_default():
-    Default.init()
+    Default.alias = None
+    Default.config = None
+    Default.instances = dict()
+    MemoryMessenger.destroy()
 
 
 @pytest.fixture(scope='function', autouse=False)
-def default():
-    os.environ['CODEPACK_ALIAS_PATH'] = 'config/alias.ini'
-    Default.init()
-    yield Default
-    Default.init()
-    os.environ.pop('CODEPACK_ALIAS_PATH', None)
+def dummy_deliveries():
+    obj1 = Delivery(id='obj1', serial_number='123', item='x')
+    obj2 = Delivery(id='obj2', serial_number='456', item='y')
+    obj3 = Delivery(id='obj3', serial_number='789', item='y')
+    yield [obj1, obj2, obj3]
diff --git a/tests/interface/__init__.py b/tests/interface/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/interface/test_interface_docker.py b/tests/interface/test_interface_docker.py
new file mode 100644
index 0000000..e8666fc
--- /dev/null
+++ b/tests/interface/test_interface_docker.py
@@ -0,0 +1,25 @@
+from unittest.mock import patch
+from codepack.interface import Docker
+
+
+@patch('docker.DockerClient')
+def test_docker_init(mock_client):
+    docker_config = {'base_url': 'unix://var/run/docker.sock'}
+    d = Docker(config=docker_config)
+    arg_list = mock_client.call_args_list
+    assert len(arg_list) == 1
+    args, kwargs = arg_list[0]
+    assert kwargs.get('base_url', '') == 'unix://var/run/docker.sock'
+    assert d.session is mock_client()
+    d.close()
+    mock_client().close.assert_called_once()
+    assert d.closed()
+
+
+@patch('docker.DockerClient')
+def test_docker_attr(mock_client):
+    docker_config = {'base_url': 'unix://var/run/docker.sock'}
+    d = Docker(config=docker_config)
+    d.images.get('test_image')
+    mock_client().images.get.assert_called_once_with('test_image')
+    d.close()
diff --git a/tests/interface/test_interface_dynamodb.py b/tests/interface/test_interface_dynamodb.py
new file mode 100644
index 0000000..d4bb29a
--- /dev/null
+++ b/tests/interface/test_interface_dynamodb.py
@@ -0,0 +1,72 @@
+from unittest.mock import patch
+from codepack.interface import DynamoDB
+import pytest
+from botocore.config import Config
+from decimal import Decimal
+
+
+@patch('boto3.client')
+def test_dynamodb_init(mock_client):
+    d = DynamoDB(config={'test_key1': 'test_value1', 'test_key2': 'test_value2'})
+    arg_list = mock_client.call_args_list
+    assert len(arg_list) == 1
+    args, kwargs = arg_list[0]
+    assert 'config' in kwargs and 'test_key1' in kwargs and 'test_key2' in kwargs
+    assert kwargs['test_key1'] == 'test_value1'
+    assert kwargs['test_key2'] == 'test_value2'
+    assert isinstance(kwargs['config'], Config)
+    assert kwargs['config'].retries == {'max_attempts': 3}
+    assert 'config' in d.config and d.config['config'] == kwargs['config']
+    assert d.session is mock_client()
+    d.close()
+    assert d.closed()
+
+
+def test_dynamodb_array_parser():
+    s1 = '1/3/5/7/9'
+    ret = DynamoDB.array_parser(s1, sep='/', dtype=int)
+    assert ret == [1, 3, 5, 7, 9]
+    s2 = '1/3//5/7/9'
+    with pytest.raises(ValueError):
+        DynamoDB.array_parser(s2, sep='/', dtype=float)
+    ret = DynamoDB.array_parser(s1, sep='/', dtype=float)
+    assert ret == [1.0, 3.0, 5.0, 7.0, 9.0]
+
+
+@patch('boto3.client')
+def test_dynamodb_list_tables(mock_client):
+    d = DynamoDB(config={})
+    mock_client.assert_called_once()
+    d.list_tables('test')
+    mock_client().list_tables.assert_called_once_with(ExclusiveStartTableName='test')
+
+
+@patch('boto3.client')
+def test_dynamodb_describe_tables(mock_client):
+    d = DynamoDB(config={})
+    d.describe_table('test')
+    mock_client().describe_table.assert_called_once_with(TableName='test')
+
+
+@patch('boto3.client')
+def test_dynamodb_query(mock_client):
+    d = DynamoDB(config={})
+    d.session.query.return_value = {'Items': [{'s_key': {'S': 's_value'}},
+                                              {'ss_key': {'SS': ['a', 'b', 'c']}},
+                                              {'n_key1': {'N': '1.23'}},
+                                              {'n_key2': {'N': '123'}}]}
+    ret = d.query(table='codepack', q="test_key = 'test_value'", columns=['c1', 'c2'])
+    assert ret == [{'s_key': 's_value'}, {'ss_key': {'a', 'b', 'c'}},
+                   {'n_key1': Decimal('1.23')}, {'n_key2': Decimal(123)}]
+    mock_client().query.assert_called_once_with(KeyConditionExpression="test_key = 'test_value'",
+                                                ProjectionExpression='c1,c2', TableName='codepack')
+
+
+@patch('boto3.client')
+def test_dynamodb_select(mock_client):
+    d = DynamoDB(config={})
+    d.session.query.return_value = dict()
+    d.select(table='codepack', columns=['c1', 'c2', 'c3'], test_key1='test_value1', test_key2=123, test_key3=1.23)
+    mock_client().query.assert_called_once_with(KeyConditionExpression=
+                                                "test_key1 = 'test_value1' and test_key2 = 123 and test_key3 = 1.23",
+                                                ProjectionExpression='c1,c2,c3', TableName='codepack')
diff --git a/tests/interface/test_interface_kafka_consumer.py b/tests/interface/test_interface_kafka_consumer.py
new file mode 100644
index 0000000..f96ee2f
--- /dev/null
+++ b/tests/interface/test_interface_kafka_consumer.py
@@ -0,0 +1,72 @@
+from unittest.mock import patch
+from codepack.interface import KafkaConsumer
+from collections.abc import Callable
+
+
+def add_1_and_raise_error(x):
+    x['test_key'] += 1
+    raise KeyboardInterrupt()
+
+
+@patch('kafka.KafkaConsumer')
+def test_kafka_consumer_init(mock_client):
+    kafka_config = {'bootstrap_servers': 'ip1:9092,ip2:9092,ip3:9092'}
+    kc = KafkaConsumer(config=kafka_config)
+    arg_list = mock_client.call_args_list
+    assert len(arg_list) == 1
+    args, kwargs = arg_list[0]
+    assert kwargs.get('bootstrap_servers', '') == 'ip1:9092,ip2:9092,ip3:9092'
+    assert isinstance(kwargs.get('value_deserializer', ''), Callable)
+    assert len(args) == 0
+    assert kc.session is mock_client()
+    kc.close()
+    mock_client().close.assert_called_once()
+    assert kc.closed()
+
+
+@patch('kafka.KafkaConsumer')
+def test_kafka_consumer_init_with_topic(mock_client):
+    kafka_config = {'bootstrap_servers': 'ip1:9092,ip2:9092,ip3:9092', 'topic': 'test_topic'}
+    kc = KafkaConsumer(config=kafka_config)
+    arg_list = mock_client.call_args_list
+    assert len(arg_list) == 1
+    args, kwargs = arg_list[0]
+    assert kwargs.get('bootstrap_servers', '') == 'ip1:9092,ip2:9092,ip3:9092'
+    assert isinstance(kwargs.get('value_deserializer', ''), Callable)
+    assert len(args) == 1 and args[0] == 'test_topic'
+    assert kc.session is mock_client()
+    kc.close()
+    mock_client().close.assert_called_once()
+    assert kc.closed()
+
+
+@patch('kafka.KafkaConsumer')
+def test_kafka_consumer_init_with_value_deserializer(mock_client):
+    kafka_config = {'bootstrap_servers': 'ip1:9092,ip2:9092,ip3:9092', 'topic': 'test_topic',
+                    'value_deserializer': lambda x: x}
+    kc = KafkaConsumer(config=kafka_config)
+    arg_list = mock_client.call_args_list
+    assert len(arg_list) == 1
+    args, kwargs = arg_list[0]
+    assert kwargs.get('bootstrap_servers', '') == 'ip1:9092,ip2:9092,ip3:9092'
+    assert isinstance(kwargs.get('value_deserializer', ''), Callable)
+    assert kwargs.get('value_deserializer')('test') == 'test'
+    assert len(args) == 1 and args[0] == 'test_topic'
+    assert kc.session is mock_client()
+    kc.close()
+    mock_client().close.assert_called_once()
+    assert kc.closed()
+
+
+@patch('kafka.KafkaConsumer')
+def test_kafka_consumer_consume(mock_client):
+    kafka_config = {'bootstrap_servers': 'ip1:9092,ip2:9092,ip3:9092', 'topic': 'test_topic'}
+    kc = KafkaConsumer(config=kafka_config)
+    tmp = {'test_key': 3}
+    kc.session.poll.return_value = tmp
+    kc.consume(add_1_and_raise_error, timeout_ms=500)
+    kc.session.poll.assert_called_once_with(timeout_ms=500)
+    assert tmp['test_key'] == 4
+    kc.close()
+    mock_client().close.assert_called_once()
+    assert kc.closed()
diff --git a/tests/interface/test_interface_kafka_producer.py b/tests/interface/test_interface_kafka_producer.py
new file mode 100644
index 0000000..32598b0
--- /dev/null
+++ b/tests/interface/test_interface_kafka_producer.py
@@ -0,0 +1,76 @@
+from unittest.mock import patch
+from codepack.interface import KafkaProducer
+from collections.abc import Callable
+import pytest
+
+
+@patch('kafka.KafkaProducer')
+def test_kafka_producer_init(mock_client):
+    kafka_config = {'bootstrap_servers': 'ip1:9092,ip2:9092,ip3:9092'}
+    kp = KafkaProducer(config=kafka_config)
+    arg_list = mock_client.call_args_list
+    assert len(arg_list) == 1
+    args, kwargs = arg_list[0]
+    assert kwargs.get('bootstrap_servers', '') == 'ip1:9092,ip2:9092,ip3:9092'
+    assert isinstance(kwargs.get('value_serializer', ''), Callable)
+    assert len(args) == 0
+    assert kp.session is mock_client()
+    kp.close()
+    mock_client().close.assert_called_once()
+    assert kp.closed()
+
+
+@patch('kafka.KafkaProducer')
+def test_kafka_producer_init_with_value_serializer(mock_client):
+    kafka_config = {'bootstrap_servers': 'ip1:9092,ip2:9092,ip3:9092', 'value_serializer': lambda x: x}
+    kp = KafkaProducer(config=kafka_config)
+    arg_list = mock_client.call_args_list
+    assert len(arg_list) == 1
+    args, kwargs = arg_list[0]
+    assert kwargs.get('bootstrap_servers', '') == 'ip1:9092,ip2:9092,ip3:9092'
+    assert isinstance(kwargs.get('value_serializer', ''), Callable)
+    assert kwargs.get('value_serializer')('test') == 'test'
+    assert kp.session is mock_client()
+    kp.close()
+    mock_client().close.assert_called_once()
+    assert kp.closed()
+
+
+@patch('kafka.KafkaProducer')
+def test_kafka_producer_produce(mock_client):
+    topic = 'test_topic'
+    kafka_config = {'bootstrap_servers': 'ip1:9092,ip2:9092,ip3:9092'}
+    kp = KafkaProducer(config=kafka_config)
+    kp.produce(topic, value={'test_key1': 'test_value', 'test_key2': 123, 'test_key3': 1.23})
+    client = mock_client()
+    client.send.assert_called_once_with(topic, value={'test_key1': 'test_value',
+                                                      'test_key2': 123, 'test_key3': 1.23})
+    client.flush.assert_called_once()
+    kp.close()
+    client.close.assert_called_once()
+    assert kp.closed()
+    with pytest.raises(AssertionError):
+        kp.produce()
+    with pytest.raises(AssertionError):
+        kp.dummy()
+
+
+@patch('kafka.KafkaProducer')
+def test_kafka_producer_getattr(mock_client):
+    topic = 'test_topic'
+    kafka_config = {'bootstrap_servers': 'ip1:9092,ip2:9092,ip3:9092'}
+    kp = KafkaProducer(config=kafka_config)
+    kp.produce(topic, value={'test_key1': 'test_value', 'test_key2': 123, 'test_key3': 1.23})
+    client = mock_client()
+    client.send.assert_called_once_with(topic, value={'test_key1': 'test_value',
+                                                      'test_key2': 123, 'test_key3': 1.23})
+    client.flush.assert_called_once()
+    kp.bootstrap_connected()
+    client.bootstrap_connected.assert_called_once()
+    kp.close()
+    client.close.assert_called_once()
+    assert kp.closed()
+    with pytest.raises(AssertionError):
+        kp.produce(topic, value={'test_key1': 'test_value', 'test_key2': 123, 'test_key3': 1.23})
+    with pytest.raises(AssertionError):
+        kp.bootstrap_connected()
diff --git a/tests/interface/test_interface_mssql.py b/tests/interface/test_interface_mssql.py
new file mode 100644
index 0000000..56be4dc
--- /dev/null
+++ b/tests/interface/test_interface_mssql.py
@@ -0,0 +1,198 @@
+from unittest.mock import patch
+from codepack.interface import MSSQL
+import pytest
+
+
+@patch('pymssql.connect')
+def test_mssql_init(mock_client):
+    mssql_config = {'host': 'localhost', 'port': 3306, 'user': 'admin', 'password': 'test', 'charset': 'utf8',
+                    'as_dict': 'False'}
+    m = MSSQL(config=mssql_config)
+    arg_list = mock_client.call_args_list
+    assert len(arg_list) == 1
+    args, kwargs = arg_list[0]
+    assert kwargs.get('host', '') == 'localhost'
+    assert kwargs.get('port', '') == 3306
+    assert kwargs.get('user', '') == 'admin'
+    assert kwargs.get('password', '') == 'test'
+    assert kwargs.get('charset', '') == 'utf8'
+    assert 'as_dict' in kwargs and isinstance(kwargs['as_dict'], bool)
+    assert not kwargs.get('as_dict', True)
+    assert m.session is mock_client()
+    m.close()
+    mock_client().close.assert_called_once()
+    assert m.closed()
+
+
+@patch('sshtunnel.SSHTunnelForwarder')
+@patch('pymssql.connect')
+def test_mssql_init_with_sshtunnel_from_config_file(mock_client, mock_ssh):
+    mssql_config = {'host': 'localhost', 'port': 3306, 'user': 'admin', 'password': 'test', 'charset': 'utf8',
+                    'as_dict': 'True', 'sshtunnel': 'config/test.ini:ssh'}
+    m = MSSQL(config=mssql_config)
+    assert m.ssh_config == {'ssh_host': 'localhost', 'ssh_port': '22', 'ssh_username': 'test', 'ssh_password': '1234'}
+    mock_ssh.assert_called_once_with(('localhost', 22), remote_bind_address=('localhost', 3306),
+                                     ssh_password='1234', ssh_username='test')
+    arg_list = mock_client.call_args_list
+    assert len(arg_list) == 1
+    args, kwargs = arg_list[0]
+    assert kwargs.get('host', '') == '127.0.0.1'
+    assert kwargs.get('port', '') != 3306
+    assert kwargs.get('user', '') == 'admin'
+    assert kwargs.get('password', '') == 'test'
+    assert kwargs.get('charset', '') == 'utf8'
+    assert 'as_dict' in kwargs and isinstance(kwargs['as_dict'], bool)
+    assert kwargs.get('as_dict', False)
+    assert m.session is mock_client()
+    m.close()
+    mock_client().close.assert_called_once()
+    mock_ssh().stop.assert_called_once()
+    assert m.closed()
+
+
+@patch('sshtunnel.SSHTunnelForwarder')
+@patch('pymssql.connect')
+def test_mssql_init_with_sshtunnel_from_dict(mock_client, mock_ssh):
+    mssql_config = {'host': 'localhost', 'port': 3306, 'user': 'admin', 'password': 'test', 'charset': 'utf8',
+                    'as_dict': 'False',
+                    'sshtunnel': {'ssh_host': 'localhost', 'ssh_port': '22',
+                                  'ssh_username': 'test', 'ssh_password': '1234'}}
+    m = MSSQL(config=mssql_config)
+    assert m.ssh_config == {'ssh_host': 'localhost', 'ssh_password': '1234', 'ssh_port': '22', 'ssh_username': 'test'}
+    mock_ssh.assert_called_once_with(('localhost', 22), remote_bind_address=('localhost', 3306),
+                                     ssh_password='1234', ssh_username='test')
+    m.close()
+    mock_client().close.assert_called_once()
+    mock_ssh().stop.assert_called_once()
+    assert m.closed()
+
+
+@patch('pymssql.connect')
+def test_mssql_select(mock_client):
+    mssql_config = {'host': 'localhost', 'port': 3306, 'user': 'admin', 'password': 'test', 'charset': 'utf8',
+                    'as_dict': 'False'}
+    m = MSSQL(config=mssql_config)
+    mock_client().cursor.return_value.rowcount = -1
+    m.select(db='codepack', table='table1', projection=['c1', 'c2', 'c3'],
+             test_key1='test_value1', test_key2=1.23, test_key3=123, test_key4=['a', 'b', 'c'])
+    mock_client().cursor.assert_called_once()
+    mock_client().cursor().execute.assert_called_once_with("select c1,c2,c3 from codepack.table1 "
+                                                           "where test_key1 = 'test_value1' "
+                                                           "and test_key2 = 1.23 and test_key3 = 123 "
+                                                           "and test_key4 in ('a', 'b', 'c')")
+    mock_client().cursor().fetchall.assert_called_once()
+    mock_client().cursor().close.assert_called_once()
+    mock_client().commit.assert_not_called()
+    mock_client().rollback.assert_not_called()
+    mock_client().cursor.return_value = None
+    with pytest.raises(AttributeError):
+        m.select(db='codepack', table='table1', projection=['c1', 'c2', 'c3'],
+                 test_key1='test_value1', test_key2=1.23, test_key3=123, test_key4=['a', 'b', 'c'])
+    mock_client().rollback.assert_called_once()
+
+
+@patch('pymssql.connect')
+def test_mssql_insert(mock_client):
+    mssql_config = {'host': 'localhost', 'port': 3306, 'user': 'admin', 'password': 'test', 'charset': 'utf8',
+                    'as_dict': 'False'}
+    m = MSSQL(config=mssql_config)
+    mock_client().cursor.return_value.rowcount = 1
+    m.insert(db='codepack', table='table1',
+             test_key1='test_value1', test_key2=1.23, test_key3=123)
+    mock_client().cursor.assert_called_once()
+    mock_client().cursor().execute.assert_called_once_with("insert into "
+                                                           "codepack.table1 (test_key1, test_key2, test_key3) "
+                                                           "values ('test_value1', 1.23, 123)")
+    mock_client().cursor().fetchall.assert_not_called()
+    mock_client().cursor().close.assert_called_once()
+    mock_client().commit.assert_not_called()
+    mock_client().rollback.assert_not_called()
+    m.insert(db='codepack', table='table1',
+             test_key1='test_value1', test_key2=1.23, test_key3=123, commit=True)
+    mock_client().cursor().fetchall.assert_not_called()
+    mock_client().commit.assert_called_once()
+    assert mock_client().cursor().close.call_count == 2
+    mock_client().rollback.assert_not_called()
+    mock_client().cursor.return_value = None
+    with pytest.raises(AttributeError):
+        m.insert(db='codepack', table='table1',
+                 test_key1='test_value1', test_key2=1.23, test_key3=123)
+    mock_client().rollback.assert_called_once()
+
+
+@patch('pymssql.connect')
+def test_mssql_insert_many(mock_client):
+    mssql_config = {'host': 'localhost', 'port': 3306, 'user': 'admin', 'password': 'test', 'charset': 'utf8',
+                    'as_dict': 'False'}
+    m = MSSQL(config=mssql_config)
+    rows = [{'test_key1': 'test_value1', 'test_key2': 1.23, 'test_key3': 123},
+            {'test_key1': 'test_value2', 'test_key2': 2.34, 'test_key3': 234},
+            {'test_key1': 'test_value3', 'test_key2': 3.45, 'test_key3': 345}]
+    m.insert_many(db='codepack', table='table1', rows=rows)
+    mock_client().cursor.assert_called_once()
+    assert mock_client().cursor().execute.call_count == 3
+    mock_client().cursor().fetchall.assert_not_called()
+    mock_client().cursor().close.assert_called_once()
+    mock_client().commit.assert_not_called()
+    mock_client().rollback.assert_not_called()
+    rows2 = [{'test_key1': 'test_value1', 'test_key2': 1.23, 'test_key3': 123},
+            {'test_key1': 'test_value2', 'test_key2': 2.34, 'test_key3': 234},
+            {'test_key1': 'test_value3', 'test_key2': 3.45, 'test_key3': 345}]
+    m.insert_many(db='codepack', table='table1', rows=rows2, commit=True)
+    assert mock_client().cursor().execute.call_count == 6
+    mock_client().cursor().fetchall.assert_not_called()
+    assert mock_client().cursor().close.call_count == 2
+    mock_client().commit.assert_called_once()
+    mock_client().rollback.assert_not_called()
+    mock_client().cursor.return_value = None
+    with pytest.raises(AttributeError):
+        m.insert_many(db='codepack', table='table1', rows=rows)
+    mock_client().rollback.assert_called_once()
+
+
+@patch('pymssql.connect')
+def test_mssql_exec(mock_client):
+    mssql_config = {'host': 'localhost', 'port': 3306, 'user': 'admin', 'passwd': 'test', 'charset': 'utf8',
+                    'as_dict': 'False'}
+    m = MSSQL(config=mssql_config)
+    m.exec(db='codepack', procedure='test_procedure', test_key1='test_value1', test_key2=1.23, test_key3=123)
+    mock_client().cursor.assert_called_once()
+    mock_client().cursor().execute.assert_called_once_with("exec codepack.test_procedure "
+                                                           "@test_key1 = 'test_value1', "
+                                                           "@test_key2 = '1.23', "
+                                                           "@test_key3 = '123'")
+    mock_client().cursor().fetchall.assert_not_called()
+    mock_client().cursor().close.assert_called_once()
+    mock_client().commit.assert_not_called()
+    mock_client().rollback.assert_not_called()
+    mock_client().cursor.return_value = None
+    with pytest.raises(AttributeError):
+        m.exec(db='codepack', procedure='test_procedure', test_key1='test_value1', test_key2=1.23, test_key3=123)
+    mock_client().rollback.assert_called_once()
+
+
+@patch('pymssql.connect')
+def test_mssql_delete(mock_client):
+    mysql_config = {'host': 'localhost', 'port': 3306, 'user': 'admin', 'passwd': 'test', 'charset': 'utf8',
+                    'as_dict': 'False'}
+    m = MSSQL(config=mysql_config)
+    m.delete(db='codepack', table='table1',
+             test_key1='test_value1', test_key2=1.23, test_key3=123, test_key4=['a', 'b', 'c'])
+    mock_client().cursor.assert_called_once()
+    mock_client().cursor().execute.assert_called_once_with("delete from codepack.table1 "
+                                                           "where test_key1 = 'test_value1' and test_key2 = 1.23 and "
+                                                           "test_key3 = 123 and test_key4 in ('a', 'b', 'c')")
+    mock_client().cursor().fetchall.assert_not_called()
+    mock_client().cursor().close.assert_called_once()
+    mock_client().commit.assert_not_called()
+    m.delete(db='codepack', table='table1',
+             test_key1='test_value1', test_key2=1.23, test_key3=123, test_key4=['a', 'b', 'c'], commit=True)
+    mock_client().cursor().fetchall.assert_not_called()
+    assert mock_client().cursor().close.call_count == 2
+    mock_client().commit.assert_called_once()
+    mock_client().rollback.assert_not_called()
+    mock_client().cursor.return_value = None
+    with pytest.raises(AttributeError):
+        m.delete(db='codepack', table='table1',
+                 test_key1='test_value1', test_key2=1.23, test_key3=123, test_key4=['a', 'b', 'c'])
+    mock_client().rollback.assert_called_once()
diff --git a/tests/interface/test_interface_mysql.py b/tests/interface/test_interface_mysql.py
new file mode 100644
index 0000000..58cde2e
--- /dev/null
+++ b/tests/interface/test_interface_mysql.py
@@ -0,0 +1,200 @@
+from unittest.mock import patch
+from codepack.interface import MySQL
+import pytest
+from pymysql.cursors import Cursor, DictCursor
+
+
+@patch('pymysql.connect')
+def test_mysql_init(mock_client):
+    mysql_config = {'host': 'localhost', 'port': 3306, 'user': 'admin', 'passwd': 'test', 'charset': 'utf8',
+                    'cursorclass': 'pymysql.cursors.DictCursor'}
+    m = MySQL(config=mysql_config)
+    arg_list = mock_client.call_args_list
+    assert len(arg_list) == 1
+    args, kwargs = arg_list[0]
+    assert kwargs.get('host', '') == 'localhost'
+    assert kwargs.get('port', '') == 3306
+    assert kwargs.get('user', '') == 'admin'
+    assert kwargs.get('passwd', '') == 'test'
+    assert kwargs.get('charset', '') == 'utf8'
+    assert 'cursorclass' in kwargs and isinstance(kwargs['cursorclass'], DictCursor.__class__)
+    assert m.session is mock_client()
+    m.close()
+    mock_client().close.assert_called_once()
+    assert m.closed()
+
+
+@patch('sshtunnel.SSHTunnelForwarder')
+@patch('pymysql.connect')
+def test_mysql_init_with_sshtunnel_from_config_file(mock_client, mock_ssh):
+    mysql_config = {'host': 'localhost', 'port': 3306, 'user': 'admin', 'passwd': 'test', 'charset': 'utf8',
+                    'cursorclass': 'pymysql.cursors.Cursor', 'sshtunnel': 'config/test.ini:ssh'}
+    m = MySQL(config=mysql_config)
+    assert m.ssh_config == {'ssh_host': 'localhost', 'ssh_port': '22', 'ssh_username': 'test', 'ssh_password': '1234'}
+    mock_ssh.assert_called_once_with(('localhost', 22), remote_bind_address=('localhost', 3306),
+                                     ssh_password='1234', ssh_username='test')
+    arg_list = mock_client.call_args_list
+    assert len(arg_list) == 1
+    args, kwargs = arg_list[0]
+    assert kwargs.get('host', '') == '127.0.0.1'
+    assert kwargs.get('port', '') != 3306
+    assert kwargs.get('user', '') == 'admin'
+    assert kwargs.get('passwd', '') == 'test'
+    assert kwargs.get('charset', '') == 'utf8'
+    assert 'cursorclass' in kwargs and isinstance(kwargs['cursorclass'], Cursor.__class__)
+    assert m.session is mock_client()
+    m.close()
+    mock_client().close.assert_called_once()
+    mock_ssh().stop.assert_called_once()
+    assert m.closed()
+
+
+@patch('sshtunnel.SSHTunnelForwarder')
+@patch('pymysql.connect')
+def test_mysql_init_with_sshtunnel_from_dict(mock_client, mock_ssh):
+    mysql_config = {'host': 'localhost', 'port': 3306, 'user': 'admin', 'passwd': 'test', 'charset': 'utf8',
+                    'cursorclass': 'pymysql.cursors.Cursor',
+                    'sshtunnel': {'ssh_host': 'localhost', 'ssh_port': '22',
+                                  'ssh_username': 'test', 'ssh_password': '1234'}}
+    m = MySQL(config=mysql_config)
+    assert m.ssh_config == {'ssh_host': 'localhost', 'ssh_password': '1234', 'ssh_port': '22', 'ssh_username': 'test'}
+    mock_ssh.assert_called_once_with(('localhost', 22), remote_bind_address=('localhost', 3306),
+                                     ssh_password='1234', ssh_username='test')
+    m.close()
+    mock_client().close.assert_called_once()
+    mock_ssh().stop.assert_called_once()
+    assert m.closed()
+
+
+@patch('pymysql.connect')
+def test_mysql_select(mock_client):
+    mysql_config = {'host': 'localhost', 'port': 3306, 'user': 'admin', 'passwd': 'test', 'charset': 'utf8',
+                    'cursorclass': 'pymysql.cursors.DictCursor'}
+    m = MySQL(config=mysql_config)
+    m.select(db='codepack', table='table1', projection=['c1', 'c2', 'c3'],
+             test_key1='test_value1', test_key2=1.23, test_key3=123)
+    mock_client().cursor.assert_called_once()
+    mock_client().cursor().execute.assert_called_once_with("select c1,c2,c3 from codepack.table1 "
+                                                           "where test_key1 = 'test_value1' "
+                                                           "and test_key2 = 1.23 and test_key3 = 123")
+    mock_client().cursor().fetchall.assert_called_once()
+    mock_client().cursor().close.assert_called_once()
+    mock_client().commit.assert_not_called()
+    mock_client().rollback.assert_not_called()
+    mock_client().cursor.return_value = None
+    with pytest.raises(AttributeError):
+        m.select(db='codepack', table='table1', projection=['c1', 'c2', 'c3'],
+                 test_key1='test_value1', test_key2=1.23, test_key3=123)
+    mock_client().rollback.assert_called_once()
+
+
+@patch('pymysql.connect')
+def test_mysql_insert(mock_client):
+    mysql_config = {'host': 'localhost', 'port': 3306, 'user': 'admin', 'passwd': 'test', 'charset': 'utf8',
+                    'cursorclass': 'pymysql.cursors.DictCursor'}
+    m = MySQL(config=mysql_config)
+    m.insert(db='codepack', table='table1',
+             test_key1='test_value1', test_key2=1.23, test_key3=123)
+    mock_client().cursor.assert_called_once()
+    mock_client().cursor().execute.assert_called_once_with("replace into "
+                                                           "codepack.table1 (test_key1, test_key2, test_key3) "
+                                                           "values ('test_value1', 1.23, 123)")
+    mock_client().cursor().fetchall.assert_called_once()
+    mock_client().cursor().close.assert_called_once()
+    mock_client().commit.assert_not_called()
+    mock_client().rollback.assert_not_called()
+    m.insert(db='codepack', table='table1',
+             test_key1='test_value1', test_key2=1.23, test_key3=123, update=False)
+    mock_client().cursor().execute.assert_called_with("insert into "
+                                                      "codepack.table1 (test_key1, test_key2, test_key3) "
+                                                      "values ('test_value1', 1.23, 123)")
+    assert mock_client().cursor().fetchall.call_count == 2
+    assert mock_client().cursor().close.call_count == 2
+    mock_client().commit.assert_not_called()
+    mock_client().rollback.assert_not_called()
+    m.insert(db='codepack', table='table1',
+             test_key1='test_value1', test_key2=1.23, test_key3=123, update=False, commit=True)
+    mock_client().commit.assert_called_once()
+    mock_client().cursor.return_value = None
+    with pytest.raises(AttributeError):
+        m.insert(db='codepack', table='table1',
+                 test_key1='test_value1', test_key2=1.23, test_key3=123)
+    mock_client().rollback.assert_called_once()
+
+
+@patch('pymysql.connect')
+def test_mysql_insert_many(mock_client):
+    mysql_config = {'host': 'localhost', 'port': 3306, 'user': 'admin', 'passwd': 'test', 'charset': 'utf8',
+                    'cursorclass': 'pymysql.cursors.DictCursor'}
+    m = MySQL(config=mysql_config)
+    rows = [{'test_key1': 'test_value1', 'test_key2': 1.23, 'test_key3': 123},
+            {'test_key1': 'test_value2', 'test_key2': 2.34, 'test_key3': 234},
+            {'test_key1': 'test_value3', 'test_key2': 3.45, 'test_key3': 345}]
+    m.insert_many(db='codepack', table='table1', rows=rows)
+    mock_client().cursor.assert_called_once()
+    assert mock_client().cursor().execute.call_count == 3
+    mock_client().cursor().fetchall.assert_called_once()
+    mock_client().cursor().close.assert_called_once()
+    mock_client().commit.assert_not_called()
+    mock_client().rollback.assert_not_called()
+    rows2 = [{'test_key1': 'test_value4', 'test_key2': 1.23, 'test_key3': 123},
+             {'test_key1': 'test_value5', 'test_key2': 2.34, 'test_key3': 234},
+             {'test_key1': 'test_value6', 'test_key2': 3.45, 'test_key3': 345}]
+    m.insert_many(db='codepack', table='table1', rows=rows2, commit=True)
+    assert mock_client().cursor().execute.call_count == 6
+    assert mock_client().cursor().fetchall.call_count == 2
+    assert mock_client().cursor().close.call_count == 2
+    mock_client().commit.assert_called_once()
+    mock_client().rollback.assert_not_called()
+    mock_client().cursor.return_value = None
+    with pytest.raises(AttributeError):
+        m.insert_many(db='codepack', table='table1', rows=rows)
+    mock_client().rollback.assert_called_once()
+
+
+@patch('pymysql.connect')
+def test_mysql_delete(mock_client):
+    mysql_config = {'host': 'localhost', 'port': 3306, 'user': 'admin', 'passwd': 'test', 'charset': 'utf8',
+                    'cursorclass': 'pymysql.cursors.DictCursor'}
+    m = MySQL(config=mysql_config)
+    m.delete(db='codepack', table='table1',
+             test_key1='test_value1', test_key2=1.23, test_key3=123, test_key4=['a', 'b', 'c'])
+    mock_client().cursor.assert_called_once()
+    mock_client().cursor().execute.assert_called_once_with("delete from codepack.table1 "
+                                                           "where test_key1 = 'test_value1' and test_key2 = 1.23 and "
+                                                           "test_key3 = 123 and test_key4 in ('a', 'b', 'c')")
+    mock_client().cursor().fetchall.assert_called_once()
+    mock_client().cursor().close.assert_called_once()
+    mock_client().commit.assert_not_called()
+    m.delete(db='codepack', table='table1',
+             test_key1='test_value1', test_key2=1.23, test_key3=123, test_key4=['a', 'b', 'c'], commit=True)
+    assert mock_client().cursor().fetchall.call_count == 2
+    assert mock_client().cursor().close.call_count == 2
+    mock_client().commit.assert_called_once()
+    mock_client().rollback.assert_not_called()
+    mock_client().cursor.return_value = None
+    with pytest.raises(AttributeError):
+        m.delete(db='codepack', table='table1',
+                 test_key1='test_value1', test_key2=1.23, test_key3=123, test_key4=['a', 'b', 'c'])
+    mock_client().rollback.assert_called_once()
+
+
+@patch('pymysql.connect')
+def test_mysql_call(mock_client):
+    mysql_config = {'host': 'localhost', 'port': 3306, 'user': 'admin', 'passwd': 'test', 'charset': 'utf8',
+                    'cursorclass': 'pymysql.cursors.DictCursor'}
+    m = MySQL(config=mysql_config)
+    m.call(db='codepack', procedure='test_procedure', test_key1='test_value1', test_key2=1.23, test_key3=123)
+    mock_client().cursor.assert_called_once()
+    mock_client().cursor().execute.assert_called_once_with("call codepack.test_procedure("
+                                                           "@test_key1 := 'test_value1', "
+                                                           "@test_key2 := '1.23', "
+                                                           "@test_key3 := '123')")
+    mock_client().cursor().fetchall.assert_called_once()
+    mock_client().cursor().close.assert_called_once()
+    mock_client().commit.assert_not_called()
+    mock_client().rollback.assert_not_called()
+    mock_client().cursor.return_value = None
+    with pytest.raises(AttributeError):
+        m.call(db='codepack', procedure='test_procedure', test_key1='test_value1', test_key2=1.23, test_key3=123)
+    mock_client().rollback.assert_called_once()
diff --git a/tests/interface/test_interface_oracledb.py b/tests/interface/test_interface_oracledb.py
new file mode 100644
index 0000000..0875727
--- /dev/null
+++ b/tests/interface/test_interface_oracledb.py
@@ -0,0 +1,66 @@
+from unittest.mock import patch
+from codepack.interface import OracleDB
+
+
+@patch('cx_Oracle.connect')
+def test_oracle_init(mock_client):
+    oracle_config = {'host': 'localhost', 'port': 3306, 'service_name': 'hello', 'as_dict': 'False'}
+    o = OracleDB(config=oracle_config, test_key='test_value')
+    arg_list = mock_client.call_args_list
+    assert len(arg_list) == 1
+    args, kwargs = arg_list[0]
+    assert kwargs.get('dsn', '') == '(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=localhost)(PORT=3306))' \
+                                    '(CONNECT_DATA=(SERVICE_NAME=hello)))'
+    assert kwargs.get('test_key', '') == 'test_value'
+    assert isinstance(o.as_dict, bool) and not o.as_dict
+    assert o.session is mock_client()
+    o.close()
+    mock_client().close.assert_called_once()
+    assert o.closed()
+    oracle_config2 = {'host': 'localhost', 'port': 3306, 'as_dict': 'True'}
+    o2 = OracleDB(config=oracle_config2, test_key2='test_value2')
+    arg_list = mock_client.call_args_list
+    assert len(arg_list) > 1
+    args, kwargs = arg_list[-1]
+    assert kwargs.get('dsn', '') == '(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=localhost)(PORT=3306))' \
+                                    '(CONNECT_DATA=))'
+    assert kwargs.get('test_key2', '') == 'test_value2'
+    assert isinstance(o2.as_dict, bool) and o2.as_dict
+
+
+@patch('sshtunnel.SSHTunnelForwarder')
+@patch('cx_Oracle.connect')
+def test_oracle_init_with_sshtunnel_from_config_file(mock_client, mock_ssh):
+    oracle_config = {'host': 'localhost', 'port': 3306, 'service_name': 'hello', 'as_dict': 'True',
+                     'sshtunnel': 'config/test.ini:ssh'}
+    o = OracleDB(config=oracle_config)
+    assert o.ssh_config == {'ssh_host': 'localhost', 'ssh_port': '22', 'ssh_username': 'test', 'ssh_password': '1234'}
+    mock_ssh.assert_called_once_with(('localhost', 22), remote_bind_address=('localhost', 3306),
+                                     ssh_password='1234', ssh_username='test')
+    arg_list = mock_client.call_args_list
+    assert len(arg_list) == 1
+    args, kwargs = arg_list[0]
+    assert kwargs.get('dsn', '') == '(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=127.0.0.1)(PORT=1))' \
+                                    '(CONNECT_DATA=(SERVICE_NAME=hello)))'
+    assert isinstance(o.as_dict, bool) and o.as_dict
+    assert o.session is mock_client()
+    o.close()
+    mock_client().close.assert_called_once()
+    mock_ssh().stop.assert_called_once()
+    assert o.closed()
+
+
+@patch('sshtunnel.SSHTunnelForwarder')
+@patch('cx_Oracle.connect')
+def test_oracle_init_with_sshtunnel_from_dict(mock_client, mock_ssh):
+    oracle_config = {'host': 'localhost', 'port': 3306, 'service_name': 'hello', 'as_dict': 'False',
+                     'sshtunnel': {'ssh_host': 'localhost', 'ssh_port': '22',
+                                   'ssh_username': 'test', 'ssh_password': '1234'}}
+    o = OracleDB(config=oracle_config)
+    assert o.ssh_config == {'ssh_host': 'localhost', 'ssh_password': '1234', 'ssh_port': '22', 'ssh_username': 'test'}
+    mock_ssh.assert_called_once_with(('localhost', 22), remote_bind_address=('localhost', 3306),
+                                     ssh_password='1234', ssh_username='test')
+    o.close()
+    mock_client().close.assert_called_once()
+    mock_ssh().stop.assert_called_once()
+    assert o.closed()
diff --git a/tests/interface/test_interface_s3.py b/tests/interface/test_interface_s3.py
new file mode 100644
index 0000000..89ce7de
--- /dev/null
+++ b/tests/interface/test_interface_s3.py
@@ -0,0 +1,90 @@
+import unittest.mock
+from unittest.mock import patch
+from codepack.interface import S3
+from botocore.config import Config
+
+
+@patch('boto3.client')
+def test_s3_init(mock_client):
+    s3_config = {'service_name': 's3', 'region_name': 'test_region', 'endpoint_url': 'test_url',
+                 'aws_access_key_id': 'test_access_key_id', 'aws_secret_access_key': 'test_secret_access_key'}
+    s = S3(config=s3_config)
+    arg_list = mock_client.call_args_list
+    assert len(arg_list) == 1
+    args, kwargs = arg_list[0]
+    assert kwargs.get('service_name', '') == 's3'
+    assert kwargs.get('region_name', '') == 'test_region'
+    assert kwargs.get('endpoint_url', '') == 'test_url'
+    assert kwargs.get('aws_access_key_id', '') == 'test_access_key_id'
+    assert kwargs.get('aws_secret_access_key', '') == 'test_secret_access_key'
+    assert isinstance(kwargs.get('config', None), Config)
+    assert kwargs.get('config').retries == {'max_attempts': 3}
+    assert 'config' in s.config and s.config['config'] == kwargs.get('config')
+    assert s.session is mock_client()
+    s.close()
+    mock_client().close.assert_not_called()
+    assert s.closed()
+
+
+@patch('boto3.client')
+def test_s3_list_objects(mock_client):
+    s3_config = {'service_name': 's3', 'region_name': 'test_region', 'endpoint_url': 'test_url',
+                 'aws_access_key_id': 'test_access_key_id', 'aws_secret_access_key': 'test_secret_access_key'}
+    s = S3(config=s3_config)
+    ret = s.list_objects(bucket='test_bucket', prefix='test_prefix')
+    mock_client().get_paginator.assert_called_once_with('list_objects')
+    mock_client().get_paginator.return_value.paginate.assert_called_once()
+    assert len(ret) == 0
+    mock_client().get_paginator.return_value.paginate.return_value = [{'Contents': ['test1']}, {'Dummy': ['test2']},
+                                                                      {'Contents': ['test3', 'test4']}]
+    ret2 = s.list_objects(bucket='test_bucket', prefix='test_prefix')
+    assert ret2 == ['test1', 'test3', 'test4']
+
+
+@patch('boto3.client')
+def test_s3_download(mock_client):
+    s3_config = {'service_name': 's3', 'region_name': 'test_region', 'endpoint_url': 'test_url',
+                 'aws_access_key_id': 'test_access_key_id', 'aws_secret_access_key': 'test_secret_access_key'}
+    s = S3(config=s3_config)
+    mc = mock_client()
+    response_body = unittest.mock.MagicMock()
+    mc.get_object.return_value.get.return_value = response_body
+    ret = s.download(bucket='test_bucket', key='test_dir/test_file', streaming=True)
+    mc.get_object.assert_called_once_with(Bucket='test_bucket', Key='test_dir/test_file')
+    response_body.read.assert_not_called()
+    assert ret is not None
+    ret = s.download(bucket='test_bucket', key='test_dir/test_file', streaming=False)
+    assert mc.get_object.call_count == 2
+    response_body.read.assert_called_once()
+    assert ret is not None
+
+
+@patch('boto3.client')
+def test_s3_exist(mock_client):
+    s3_config = {'service_name': 's3', 'region_name': 'test_region', 'endpoint_url': 'test_url',
+                 'aws_access_key_id': 'test_access_key_id', 'aws_secret_access_key': 'test_secret_access_key'}
+    s = S3(config=s3_config)
+    ret = s.exist(bucket='test_bucket', key='test_dir/test_file')
+    mc = mock_client()
+    mc.head_object.assert_called_once_with(Bucket='test_bucket', Key='test_dir/test_file')
+    assert ret is not False
+
+
+@patch('boto3.client')
+def test_s3_delete(mock_client):
+    s3_config = {'service_name': 's3', 'region_name': 'test_region', 'endpoint_url': 'test_url',
+                 'aws_access_key_id': 'test_access_key_id', 'aws_secret_access_key': 'test_secret_access_key'}
+    s = S3(config=s3_config)
+    s.delete(bucket='test_bucket', key='test_dir/test_file')
+    mc = mock_client()
+    mc.delete_object.assert_called_once_with(Bucket='test_bucket', Key='test_dir/test_file')
+
+
+@patch('boto3.client')
+def test_s3_upload(mock_client):
+    s3_config = {'service_name': 's3', 'region_name': 'test_region', 'endpoint_url': 'test_url',
+                 'aws_access_key_id': 'test_access_key_id', 'aws_secret_access_key': 'test_secret_access_key'}
+    s = S3(config=s3_config)
+    s.upload(bucket='test_bucket', key='test_dir/test_file', data=b'12345678')
+    mc = mock_client()
+    mc.put_object.assert_called_once_with(Bucket='test_bucket', Key='test_dir/test_file', Body=b'12345678')
diff --git a/tests/service/__init__.py b/tests/service/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_delivery_service.py b/tests/service/test_delivery_service.py
similarity index 60%
rename from tests/test_delivery_service.py
rename to tests/service/test_delivery_service.py
index 94bb9ee..d19a3c1 100644
--- a/tests/test_delivery_service.py
+++ b/tests/service/test_delivery_service.py
@@ -1,27 +1,31 @@
-from codepack.service import MemoryDeliveryService, FileDeliveryService, MongoDeliveryService
-from codepack.delivery import Delivery
+from codepack import DeliveryService, Delivery
+from codepack.storage import MemoryStorage, FileStorage, MongoStorage
 import os
 
 
 def test_memory_delivery_service_check():
-    mds = MemoryDeliveryService(item_type=Delivery)
-    mds.init()
+    storage = MemoryStorage(item_type=Delivery)
+    mds = DeliveryService(storage=storage)
+    mds.storage.init()
     sender = 'sender'
     serial_number = '1234'
     item = 'Hello, World!'
     mds.send(id=sender, serial_number=serial_number, item=item)
-    assert isinstance(mds.check(serial_number=serial_number), dict)
+    assert isinstance(mds.check(serial_number=serial_number), bool)
     mds.cancel(serial_number=serial_number)
     serial_numbers = ['123', '456', '789']
     mds.send(id=sender, serial_number=serial_numbers[0], item=item)
     mds.send(id=sender, serial_number=serial_numbers[1], item=item)
     check = mds.check(serial_number=serial_numbers)
-    assert isinstance(check, list)
-    assert len(check) == 2
+    assert isinstance(check, bool)
+    assert check is False
+    mds.send(id=sender, serial_number=serial_numbers[2], item=item)
+    assert mds.check(serial_number=serial_numbers) is True
 
 
 def test_file_delivery_service_check(testdir_delivery_service):
-    fds = FileDeliveryService(item_type=Delivery, path=testdir_delivery_service)
+    storage = FileStorage(item_type=Delivery, path=testdir_delivery_service)
+    fds = DeliveryService(storage=storage)
     sender = 'sender'
     serial_number = '1234'
     serial_numbers = ['123', '456', '789']
@@ -36,19 +40,27 @@ def test_file_delivery_service_check(testdir_delivery_service):
             continue
     item = 'Hello, World!'
     fds.send(id=sender, serial_number=serial_number, item=item)
-    assert isinstance(fds.check(serial_number=serial_number), dict)
+    check = fds.check(serial_number=serial_number)
+    assert isinstance(check, bool)
+    assert check is True
     fds.cancel(serial_number=serial_number)
+    check = fds.check(serial_number=serial_number)
+    assert check is False
     fds.send(id=sender, serial_number=serial_numbers[0], item=item)
     fds.send(id=sender, serial_number=serial_numbers[1], item=item)
     check = fds.check(serial_number=serial_numbers)
-    assert isinstance(check, list)
-    assert len(check) == 2
+    assert isinstance(check, bool)
+    assert check is False
+    fds.send(id=sender, serial_number=serial_numbers[2], item=item)
+    check = fds.check(serial_number=serial_numbers)
+    assert check is True
 
 
 def test_mongo_delivery_service_check(fake_mongodb):
     db = 'test'
     collection = 'cache'
-    mds = MongoDeliveryService(item_type=Delivery, mongodb=fake_mongodb, db=db, collection=collection)
+    storage = MongoStorage(item_type=Delivery, mongodb=fake_mongodb, db=db, collection=collection)
+    mds = DeliveryService(storage=storage)
     sender = 'sender'
     serial_number = '1234'
     serial_numbers = ['123', '456', '789']
@@ -57,26 +69,31 @@ def test_mongo_delivery_service_check(fake_mongodb):
         mds.cancel(serial_number=i)
     item = 'Hello, World!'
     mds.send(id=sender, serial_number=serial_number, item=item)
-    assert isinstance(mds.check(serial_number=serial_number), dict)
+    check = mds.check(serial_number=serial_number)
+    assert isinstance(check, bool)
+    assert check is True
     mds.cancel(serial_number=serial_number)
     mds.send(id=sender, serial_number=serial_numbers[0], item=item)
     mds.send(id=sender, serial_number=serial_numbers[1], item=item)
     check = mds.check(serial_number=serial_numbers)
-    assert isinstance(check, list)
-    assert len(check) == 2
+    assert isinstance(check, bool)
+    assert check is False
+    mds.send(id=sender, serial_number=serial_numbers[2], item=item)
+    assert mds.check(serial_number=serial_numbers) is True
 
 
 def test_memory_delivery_service():
-    mds = MemoryDeliveryService(item_type=Delivery)
-    mds.init()
+    storage = MemoryStorage(item_type=Delivery)
+    mds = DeliveryService(storage=storage)
+    mds.storage.init()
     sender = 'sender'
     serial_number = '1234'
     item = 'Hello, World!'
     mds.send(id=sender, serial_number=serial_number, item=item)
-    assert serial_number in mds.memory, "'send' failed"
+    assert serial_number in storage.memory, "'send' failed"
     check = mds.check(serial_number=serial_number)
     receive = mds.receive(serial_number=serial_number)
-    assert check['id'] == sender and check['_id'] == serial_number, "'check' failed"
+    assert check is True
     assert receive == item, "'receive' failed"
     mds.cancel(serial_number=serial_number)
     check2 = mds.check(serial_number=serial_number)
@@ -84,25 +101,27 @@ def test_memory_delivery_service():
 
 
 def test_file_delivery_service(testdir_delivery_service):
-    fds = FileDeliveryService(item_type=Delivery, path=testdir_delivery_service)
+    storage = FileStorage(item_type=Delivery, path=testdir_delivery_service)
+    fds = DeliveryService(storage=storage)
     sender = 'sender'
     serial_number = '1234'
     item = 'Hello, World!'
     fds.send(id=sender, serial_number=serial_number, item=item)
-    assert os.path.isfile(Delivery.get_path(serial_number=serial_number, path=fds.path)), "'send' failed"
+    assert os.path.isfile(Delivery.get_path(key=serial_number, path=storage.path)), "'send' failed"
     check = fds.check(serial_number=serial_number)
     receive = fds.receive(serial_number=serial_number)
-    assert check['id'] == sender and check['_id'] == serial_number, "'check' failed"
+    assert check is True
     assert receive == item, "'receive' error"
     fds.cancel(serial_number=serial_number)
     check2 = fds.check(serial_number=serial_number)
-    assert not check2, "'cancel' error"
+    assert check2 is False, "'cancel' error"
 
 
 def test_mongo_delivery_service(fake_mongodb):
     db = 'test'
     collection = 'cache'
-    mds = MongoDeliveryService(item_type=Delivery, mongodb=fake_mongodb, db=db, collection=collection)
+    storage = MongoStorage(item_type=Delivery, mongodb=fake_mongodb, db=db, collection=collection)
+    mds = DeliveryService(storage=storage)
     sender = 'sender'
     serial_number = '1234'
     item = 'Hello, World!'
@@ -110,8 +129,8 @@ def test_mongo_delivery_service(fake_mongodb):
     assert fake_mongodb[db][collection].find_one({'_id': serial_number}), "'send' failed"
     check = mds.check(serial_number=serial_number)
     receive = mds.receive(serial_number=serial_number)
-    assert check['id'] == sender and check['_id'] == serial_number, "'check' failed"
+    assert check is True, "'check' failed"
     assert receive == item, "'receive' error"
     mds.cancel(serial_number=serial_number)
     check2 = mds.check(serial_number=serial_number)
-    assert not check2, "'cancel' error"
+    assert check2 is False, "'cancel' error"
diff --git a/tests/test_snapshot_service.py b/tests/service/test_snapshot_service.py
similarity index 75%
rename from tests/test_snapshot_service.py
rename to tests/service/test_snapshot_service.py
index bbbbd4a..daa4a8c 100644
--- a/tests/test_snapshot_service.py
+++ b/tests/service/test_snapshot_service.py
@@ -1,21 +1,20 @@
-from codepack.service import MemorySnapshotService, FileSnapshotService, MongoSnapshotService
-from codepack.snapshot import Snapshot
-from codepack.snapshot import CodeSnapshot
-from codepack import Code
+from codepack import Code, SnapshotService, Snapshot, CodeSnapshot
+from codepack.storage import MemoryStorage, FileStorage, MongoStorage
 from datetime import datetime
 from tests import *
 import os
 
 
 def test_memory_code_snapshot_service_save_and_load(default_os_env):
-    mss = MemorySnapshotService(item_type=CodeSnapshot)
-    mss.init()
+    storage = MemoryStorage(item_type=CodeSnapshot)
+    mss = SnapshotService(storage=storage)
+    mss.storage.init()
     code = Code(add2)
     snapshot = CodeSnapshot(code)
     mss.save(snapshot=snapshot)
-    assert len(mss.memory) == 1
+    assert len(storage.memory) == 1
     assert mss.load(snapshot.serial_number) == snapshot.to_dict()
-    loaded = mss.load(snapshot.serial_number, projection={'state'})
+    loaded = mss.load(snapshot.serial_number, projection=['state'])
     assert set(loaded.keys()) == {'serial_number', 'state'}
     assert 'state' in loaded
     assert loaded['state'] == 'UNKNOWN'
@@ -24,8 +23,9 @@ def test_memory_code_snapshot_service_save_and_load(default_os_env):
 
 
 def test_memory_code_snapshot_service_search_and_remove(default_os_env):
-    mss = MemorySnapshotService(item_type=CodeSnapshot)
-    mss.init()
+    storage = MemoryStorage(item_type=CodeSnapshot)
+    mss = SnapshotService(storage=storage)
+    mss.storage.init()
     code1 = Code(add2)
     code2 = Code(add3)
     code1 >> code2
@@ -34,18 +34,18 @@ def test_memory_code_snapshot_service_search_and_remove(default_os_env):
     snapshot2 = CodeSnapshot(code2)
     mss.save(snapshot=snapshot1)
     mss.save(snapshot=snapshot2)
-    assert len(mss.memory) == 2
+    assert len(storage.memory) == 2
     loaded = mss.load([snapshot1.serial_number, snapshot2.serial_number, '1234'])
     assert len(loaded) == 2
     assert loaded[0]['state'] == 'UNKNOWN'
     assert loaded[1]['state'] == 'WAITING'
-    loaded = mss.load([snapshot1.serial_number, snapshot2.serial_number, '1234'], projection={'state'})
+    loaded = mss.load([snapshot1.serial_number, snapshot2.serial_number, '1234'], projection=['state'])
     for x in loaded:
         assert set(x.keys()) == {'state', 'serial_number'}
     search_result = mss.search(key='state', value='WAITING')
     assert len(search_result) == 1
     assert search_result[0] == snapshot2.to_dict()
-    search_result = mss.search(key='state', value='WAITING', projection={'state'})
+    search_result = mss.search(key='state', value='WAITING', projection=['state'])
     assert len(search_result) == 1
     assert len(search_result[0].keys()) == 2
     assert search_result[0]['state'] == 'WAITING'
@@ -53,32 +53,41 @@ def test_memory_code_snapshot_service_search_and_remove(default_os_env):
     mss.remove(snapshot2.serial_number)
     loaded = mss.load([snapshot1.serial_number, snapshot2.serial_number, '1234'])
     assert len(loaded) == 1
-    search_result = mss.search(key='state', value='WAITING', projection={'state'})
+    search_result = mss.search(key='state', value='WAITING', projection=['state'])
     assert len(search_result) == 0
 
 
 def test_memory_snapshot_service_update():
-    mss = MemorySnapshotService(item_type=Snapshot)
-    mss.init()
+    storage = MemoryStorage(item_type=Snapshot)
+    mss = SnapshotService(storage=storage)
+    mss.storage.init()
     timestamp = datetime.now().timestamp()
     snapshot1 = Snapshot(id='1234', serial_number='5678', timestamp=timestamp)
     snapshot2 = Snapshot(id='1234', serial_number='5678', timestamp=timestamp + 1)
     mss.save(snapshot=snapshot1)
-    tmp_id = id(mss.memory[snapshot1.serial_number])
     mss.save(snapshot=snapshot2)
-    assert len(mss.memory) == 1
-    assert tmp_id == id(mss.memory[snapshot2.serial_number])
-    assert mss.memory[snapshot1.serial_number].timestamp == timestamp + 1
+    assert len(storage.memory) == 1
+    recent_instance = storage.memory[snapshot2.serial_number]
+    assert id(snapshot1) != id(recent_instance)
+    assert id(snapshot2) != id(recent_instance)
+    assert snapshot1.id == recent_instance.id
+    assert snapshot1.serial_number == recent_instance.serial_number
+    assert snapshot1.timestamp + 1 == recent_instance.timestamp
+    snapshot3 = Snapshot(id='1234', serial_number='5678', timestamp=timestamp + 1)
+    mss.save(snapshot=snapshot3)
+    recent_instance2 = storage.memory[snapshot2.serial_number]
+    assert id(recent_instance) == id(recent_instance2)
 
 
 def test_file_code_snapshot_service_save_and_load(default_os_env, testdir_snapshot_service):
-    fss = FileSnapshotService(item_type=CodeSnapshot, path=testdir_snapshot_service)
+    storage = FileStorage(item_type=CodeSnapshot, path=testdir_snapshot_service)
+    fss = SnapshotService(storage=storage)
     code = Code(add2)
     snapshot = CodeSnapshot(code)
     fss.save(snapshot=snapshot)
     assert len(os.listdir(testdir_snapshot_service)) == 1
     assert fss.load(snapshot.serial_number) == snapshot.to_dict()
-    loaded = fss.load(snapshot.serial_number, projection={'state'})
+    loaded = fss.load(snapshot.serial_number, projection=['state'])
     assert set(loaded.keys()) == {'serial_number', 'state'}
     assert 'state' in loaded
     assert loaded['state'] == 'UNKNOWN'
@@ -87,7 +96,8 @@ def test_file_code_snapshot_service_save_and_load(default_os_env, testdir_snapsh
 
 
 def test_file_code_snapshot_service_search_and_remove(default_os_env, testdir_snapshot_service):
-    fss = FileSnapshotService(item_type=CodeSnapshot, path=testdir_snapshot_service)
+    storage = FileStorage(item_type=CodeSnapshot, path=testdir_snapshot_service)
+    fss = SnapshotService(storage=storage)
     code1 = Code(add2)
     code2 = Code(add3)
     code1 >> code2
@@ -101,13 +111,13 @@ def test_file_code_snapshot_service_search_and_remove(default_os_env, testdir_sn
     assert len(loaded) == 2
     assert loaded[0]['state'] == 'UNKNOWN'
     assert loaded[1]['state'] == 'WAITING'
-    loaded = fss.load([snapshot1.serial_number, snapshot2.serial_number, '1234'], projection={'state'})
+    loaded = fss.load([snapshot1.serial_number, snapshot2.serial_number, '1234'], projection=['state'])
     for x in loaded:
         assert set(x.keys()) == {'state', 'serial_number'}
     search_result = fss.search(key='state', value='WAITING')
     assert len(search_result) == 1
     assert search_result[0] == snapshot2.to_dict()
-    search_result = fss.search(key='state', value='WAITING', projection={'state'})
+    search_result = fss.search(key='state', value='WAITING', projection=['state'])
     assert len(search_result) == 1
     assert len(search_result[0].keys()) == 2
     assert search_result[0]['state'] == 'WAITING'
@@ -115,19 +125,20 @@ def test_file_code_snapshot_service_search_and_remove(default_os_env, testdir_sn
     fss.remove(snapshot2.serial_number)
     loaded = fss.load([snapshot1.serial_number, snapshot2.serial_number, '1234'])
     assert len(loaded) == 1
-    search_result = fss.search(key='state', value='WAITING', projection={'state'})
+    search_result = fss.search(key='state', value='WAITING', projection=['state'])
     assert len(search_result) == 0
 
 
 def test_file_snapshot_service_update(testdir_snapshot_service):
-    fss = FileSnapshotService(item_type=Snapshot, path=testdir_snapshot_service)
+    storage = FileStorage(item_type=Snapshot, path=testdir_snapshot_service)
+    fss = SnapshotService(storage=storage)
     timestamp = datetime.now().timestamp()
     snapshot1 = Snapshot(id='1234', serial_number='5678', timestamp=timestamp)
     snapshot2 = Snapshot(id='1234', serial_number='5678', timestamp=timestamp + 1)
     fss.save(snapshot=snapshot1)
     fss.save(snapshot=snapshot2)
     assert len(os.listdir(testdir_snapshot_service)) == 1
-    search_result = fss.search(key='id', value='1234', projection={'timestamp'})
+    search_result = fss.search(key='id', value='1234', projection=['timestamp'])
     assert len(search_result) == 1
     assert search_result[0]['timestamp'] == timestamp + 1
 
@@ -135,13 +146,14 @@ def test_file_snapshot_service_update(testdir_snapshot_service):
 def test_mongo_code_snapshot_service_save_and_load(default_os_env, fake_mongodb):
     db = 'test'
     collection = 'snapshot'
-    mss = MongoSnapshotService(item_type=CodeSnapshot, mongodb=fake_mongodb, db=db, collection=collection)
+    storage = MongoStorage(item_type=CodeSnapshot, mongodb=fake_mongodb, db=db, collection=collection)
+    mss = SnapshotService(storage=storage)
     code = Code(add2)
     snapshot = CodeSnapshot(code)
     mss.save(snapshot=snapshot)
     assert fake_mongodb[db][collection].count_documents({}) == 1
     assert mss.load(snapshot.serial_number) == snapshot.to_dict()
-    loaded = mss.load(snapshot.serial_number, projection={'state'})
+    loaded = mss.load(snapshot.serial_number, projection=['state'])
     assert set(loaded.keys()) == {'serial_number', 'state'}
     assert 'state' in loaded
     assert loaded['state'] == 'UNKNOWN'
@@ -152,7 +164,8 @@ def test_mongo_code_snapshot_service_save_and_load(default_os_env, fake_mongodb)
 def test_mongo_code_snapshot_service_search_and_remove(default_os_env, fake_mongodb):
     db = 'test'
     collection = 'snapshot'
-    mss = MongoSnapshotService(item_type=CodeSnapshot, mongodb=fake_mongodb, db=db, collection=collection)
+    storage = MongoStorage(item_type=CodeSnapshot, mongodb=fake_mongodb, db=db, collection=collection)
+    mss = SnapshotService(storage=storage)
     code1 = Code(add2)
     code2 = Code(add3)
     code1 >> code2
@@ -166,13 +179,13 @@ def test_mongo_code_snapshot_service_search_and_remove(default_os_env, fake_mong
     assert len(loaded) == 2
     assert loaded[0]['state'] == 'UNKNOWN'
     assert loaded[1]['state'] == 'WAITING'
-    loaded = mss.load([snapshot1.serial_number, snapshot2.serial_number, '1234'], projection={'state'})
+    loaded = mss.load([snapshot1.serial_number, snapshot2.serial_number, '1234'], projection=['state'])
     for x in loaded:
         assert set(x.keys()) == {'state', 'serial_number'}
     search_result = mss.search(key='state', value='WAITING')
     assert len(search_result) == 1
     assert search_result[0] == snapshot2.to_dict()
-    search_result = mss.search(key='state', value='WAITING', projection={'state'})
+    search_result = mss.search(key='state', value='WAITING', projection=['state'])
     assert len(search_result) == 1
     assert len(search_result[0].keys()) == 2
     assert search_result[0]['state'] == 'WAITING'
@@ -180,20 +193,21 @@ def test_mongo_code_snapshot_service_search_and_remove(default_os_env, fake_mong
     mss.remove(snapshot2.serial_number)
     loaded = mss.load([snapshot1.serial_number, snapshot2.serial_number, '1234'])
     assert len(loaded) == 1
-    search_result = mss.search(key='state', value='WAITING', projection={'state'})
+    search_result = mss.search(key='state', value='WAITING', projection=['state'])
     assert len(search_result) == 0
 
 
 def test_mongo_snapshot_service_update(fake_mongodb):
     db = 'test'
     collection = 'snapshot'
-    mss = MongoSnapshotService(item_type=Snapshot, mongodb=fake_mongodb, db=db, collection=collection)
+    storage = MongoStorage(item_type=Snapshot, mongodb=fake_mongodb, db=db, collection=collection)
+    mss = SnapshotService(storage=storage)
     timestamp = datetime.now().timestamp()
     snapshot1 = Snapshot(id='1234', serial_number='5678', timestamp=timestamp)
     snapshot2 = Snapshot(id='1234', serial_number='5678', timestamp=timestamp + 1)
     mss.save(snapshot=snapshot1)
     mss.save(snapshot=snapshot2)
     assert fake_mongodb[db][collection].count_documents({}) == 1
-    search_result = mss.search(key='id', value='1234', projection={'timestamp'})
+    search_result = mss.search(key='id', value='1234', projection=['timestamp'])
     assert len(search_result) == 1
     assert search_result[0]['timestamp'] == timestamp + 1
diff --git a/tests/test_storage_service.py b/tests/service/test_storage_service.py
similarity index 55%
rename from tests/test_storage_service.py
rename to tests/service/test_storage_service.py
index 12f2561..06fbdc1 100644
--- a/tests/test_storage_service.py
+++ b/tests/service/test_storage_service.py
@@ -1,29 +1,36 @@
-from codepack import Code
-from codepack.service import MemoryStorageService, FileStorageService, MongoStorageService
+from codepack import Code, StorageService
+from codepack.storage import MemoryStorage, FileStorage, MongoStorage
 from tests import *
 import os
 
 
 def test_memory_storage_service_check(default_os_env):
-    mss = MemoryStorageService(item_type=Code)
-    mss.init()
+    storage = MemoryStorage(item_type=Code)
+    assert storage.key == 'serial_number'
+    mss = StorageService(storage=storage)
+    assert storage.key == 'id'
+    mss.storage.init()
     code1 = Code(hello, id='hello1', storage_service=mss)
     code2 = Code(hello, id='hello2', storage_service=mss)
     code3 = Code(hello, id='hello3', storage_service=mss)
     code1.save()
-    assert isinstance(mss.check(id=code1.id), str)
-    assert code1.id in mss.memory
+    assert isinstance(mss.check(id=code1.id), bool)
+    assert code1.id in storage.memory
     mss.remove(id=code1.id)
     code2.save()
     code3.save()
-    assert len(mss.memory) == 2
+    assert len(storage.memory) == 2
     check = mss.check([code1.id, code2.id, code3.id])
     assert isinstance(check, list)
-    assert len(check) == 2
+    assert len(check) == 3
+    assert not check[0] and check[1] and check[2]
 
 
 def test_file_storage_service_check(default_os_env, testdir_storage_service):
-    fss = FileStorageService(item_type=Code, path=testdir_storage_service)
+    storage = FileStorage(item_type=Code, path=testdir_storage_service)
+    assert storage.key == 'serial_number'
+    fss = StorageService(storage=storage)
+    assert storage.key == 'id'
     code1 = Code(hello, id='hello1', storage_service=fss)
     code2 = Code(hello, id='hello2', storage_service=fss)
     code3 = Code(hello, id='hello3', storage_service=fss)
@@ -36,19 +43,22 @@ def test_file_storage_service_check(default_os_env, testdir_storage_service):
     code2.save()
     check = fss.check([code1.id, code2.id, code3.id])
     assert isinstance(check, list)
-    assert code1.id in check and code2.id in check and code3.id not in check
+    assert check[0] and check[1] and not check[2]
     fss.remove(id=code1.id)
     code3.save()
     check = fss.check([code1.id, code2.id, code3.id])
     assert isinstance(check, list)
-    assert code1.id not in check and code2.id in check and code3.id in check
-    assert len(check) == 2
+    assert not check[0] and check[1] and check[2]
+    assert len(check) == 3
 
 
 def test_mongo_storage_service_check(default_os_env, fake_mongodb):
     db = 'test'
     collection = 'codes'
-    mss = MongoStorageService(item_type=Code, mongodb=fake_mongodb, db=db, collection=collection)
+    storage = MongoStorage(item_type=Code, mongodb=fake_mongodb, db=db, collection=collection)
+    assert storage.key == 'serial_number'
+    mss = StorageService(storage=storage)
+    assert storage.key == 'id'
     code1 = Code(hello, id='hello1', storage_service=mss)
     code2 = Code(hello, id='hello2', storage_service=mss)
     code3 = Code(hello, id='hello3', storage_service=mss)
@@ -61,64 +71,67 @@ def test_mongo_storage_service_check(default_os_env, fake_mongodb):
     code2.save()
     check = mss.check([code1.id, code2.id, code3.id])
     assert isinstance(check, list)
-    assert code1.id in check and code2.id in check and code3.id not in check
+    assert check[0] and check[1] and not check[2]
     mss.remove(id=code1.id)
     code3.save()
     check = mss.check([code1.id, code2.id, code3.id])
     assert isinstance(check, list)
-    assert code1.id not in check and code2.id in check and code3.id in check
-    assert len(check) == 2
+    assert not check[0] and check[1] and check[2]
+    assert len(check) == 3
 
 
 def test_memory_storage_service(default_os_env):
-    mss = MemoryStorageService(item_type=Code)
-    mss.init()
+    storage = MemoryStorage(item_type=Code)
+    mss = StorageService(storage=storage)
+    mss.storage.init()
     code1 = Code(hello, storage_service=mss)
     code2 = Code(add2, storage_service=mss)
     code1.save()
-    assert len(mss.memory) == 1
-    assert code1.id in mss.memory
-    assert code2.id not in mss.memory
+    assert len(storage.memory) == 1
+    assert code1.id in storage.memory
+    assert code2.id not in storage.memory
     code3 = mss.load(code1.id)
     assert code1.id == code3.id
     assert code1.source == code3.source
     assert code1("CodePack") == code3("CodePack")
     mss.remove(code3.id)
-    assert len(mss.memory) == 0
+    assert len(storage.memory) == 0
 
 
 def test_file_storage_service(default_os_env, testdir_storage_service):
     filepath = testdir_storage_service
-    fss = FileStorageService(item_type=Code, path=filepath)
-    assert fss.path == filepath
+    storage = FileStorage(item_type=Code, path=filepath)
+    fss = StorageService(storage=storage)
+    assert storage.path == filepath
     code1 = Code(hello, storage_service=fss)
     code2 = Code(add2, storage_service=fss)
     code1.save()
-    assert not os.path.isfile(Code.get_path(serial_number=code1.id))
-    assert os.path.isfile(Code.get_path(serial_number=code1.id, path=filepath))
-    assert not os.path.isfile(Code.get_path(serial_number=code2.id, path=filepath))
+    assert not os.path.isfile(Code.get_path(key=code1.id))
+    assert os.path.isfile(Code.get_path(key=code1.id, path=filepath))
+    assert not os.path.isfile(Code.get_path(key=code2.id, path=filepath))
     code3 = fss.load(code1.id)
     assert code1.id == code3.id
     assert code1.source.strip() == code3.source.strip()
     assert code1("CodePack") == code3("CodePack")
     fss.remove(code3.id)
-    assert not os.path.isfile(Code.get_path(serial_number=code1.id, path=filepath))
+    assert not os.path.isfile(Code.get_path(key=code1.id, path=filepath))
 
 
 def test_mongo_storage_service(default_os_env, fake_mongodb):
     db = 'test'
     collection = 'codes'
-    mss = MongoStorageService(item_type=Code, mongodb=fake_mongodb, db=db, collection=collection)
-    assert mss.item_type == Code
+    storage = MongoStorage(item_type=Code, mongodb=fake_mongodb, db=db, collection=collection)
+    mss = StorageService(storage=storage)
+    assert mss.storage.item_type == Code
     code1 = Code(hello, storage_service=mss)
     code2 = Code(add2, storage_service=mss)
     code1.save()
-    assert mss.mongodb[db][collection].count_documents({'_id': code1.id}) == 1
-    assert mss.mongodb[db][collection].count_documents({'_id': code2.id}) == 0
-    assert mss.item_type == Code
+    assert storage.mongodb[db][collection].count_documents({'_id': code1.id}) == 1
+    assert storage.mongodb[db][collection].count_documents({'_id': code2.id}) == 0
+    assert storage.item_type == Code
     code3 = mss.load(code1.id)
     assert code1.id == code3.id
     assert code1.source.strip() == code3.source.strip()
     assert code1("CodePack") == code3("CodePack")
     mss.remove(code3.id)
-    assert mss.mongodb[db][collection].count_documents({'_id': code1.id}) == 0
+    assert storage.mongodb[db][collection].count_documents({'_id': code1.id}) == 0
diff --git a/tests/storage/__init__.py b/tests/storage/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/storage/test_storage.py b/tests/storage/test_storage.py
new file mode 100644
index 0000000..57e440f
--- /dev/null
+++ b/tests/storage/test_storage.py
@@ -0,0 +1,208 @@
+from codepack import Delivery, Config
+from codepack.storage import MemoryStorage, FileStorage, MongoStorage, S3Storage
+import pytest
+import json
+from unittest.mock import patch
+import os
+
+
+def test_memory_storage(dummy_deliveries):
+    storage = MemoryStorage(item_type=Delivery, key='id')
+    assert storage.key == 'id'
+    storage.save(item=dummy_deliveries)
+    assert sorted(storage.memory.keys()) == sorted([d.id for d in dummy_deliveries])
+    with pytest.raises(ValueError):
+        storage.save(item=dummy_deliveries[0])
+    assert dummy_deliveries[0].item == 'x'
+    dummy_deliveries[0].send(item='z')
+    storage.save(item=dummy_deliveries[0], update=True)
+    storage.remove(key=[dummy_deliveries[1].id, dummy_deliveries[2].id])
+    assert storage.exist(key=[d.id for d in dummy_deliveries]) == [True, False, False]
+    assert storage.exist(key=[d.id for d in dummy_deliveries], summary='and') is False
+    assert storage.exist(key=[d.id for d in dummy_deliveries], summary='or') is True
+    storage.save(item=[dummy_deliveries[1], dummy_deliveries[2]])
+    assert storage.exist(key=[d.id for d in dummy_deliveries]) == [True, True, True]
+    assert storage.exist(key=[d.id for d in dummy_deliveries], summary='and') is True
+    assert storage.exist(key=[d.id for d in dummy_deliveries], summary='or') is True
+    search_result = storage.search(key='item', value=json.dumps('?'))
+    assert type(search_result) == list and len(search_result) == 0
+    ref = sorted([dummy_deliveries[1].id, dummy_deliveries[2].id])
+    search_result = storage.search(key='item', value=json.dumps('z'))
+    assert len(search_result) == 1 and search_result[0] == dummy_deliveries[0]
+    search_result = storage.search(key='item', value=json.dumps('y'))
+    assert len(search_result) == 2
+    assert sorted([s.id for s in search_result]) == ref
+    search_result = storage.search(key='item', value=json.dumps('y'), to_dict=True)
+    assert len(search_result) == 2
+    assert sorted([s['id'] for s in search_result]) == ref
+    assert type(search_result[0]) == dict and type(search_result[1]) == dict
+    assert set(search_result[0].keys()) == {'item', 'id', '_id', 'timestamp', 'serial_number'}
+    assert set(search_result[1].keys()) == {'item', 'id', '_id', 'timestamp', 'serial_number'}
+    search_result = storage.search(key='item', value=json.dumps('y'), projection=['item'])
+    assert len(search_result) == 2
+    assert type(search_result[0]) == dict and type(search_result[1]) == dict
+    assert set(search_result[0].keys()) == {'item', 'id'}
+    assert set(search_result[1].keys()) == {'item', 'id'}
+    load_result = storage.load(key='???')
+    assert load_result is None
+    load_result = storage.load(key=['!??', '?!?', '??!'])
+    assert type(load_result) == list and len(load_result) == 0
+    load_result = storage.load(key=[d.id for d in dummy_deliveries])
+    assert type(load_result) == list and load_result == dummy_deliveries
+    load_result = storage.load(key=dummy_deliveries[1].id)
+    assert isinstance(load_result, Delivery)
+    load_result = storage.load(key=[dummy_deliveries[1].id, dummy_deliveries[2].id, '???'], to_dict=True)
+    assert type(load_result) == list and len(load_result) == 2
+    assert type(load_result[0]) == dict and type(load_result[1]) == dict
+    assert set(load_result[0].keys()) == {'item', 'id', '_id', 'timestamp', 'serial_number'}
+    assert set(load_result[1].keys()) == {'item', 'id', '_id', 'timestamp', 'serial_number'}
+    load_result = storage.load(key=[dummy_deliveries[1].id, dummy_deliveries[2].id], projection=['timestamp', '_id'])
+    assert type(load_result) == list
+    assert type(load_result[0]) == dict and type(load_result[1]) == dict
+    assert set(load_result[0].keys()) == {'id', '_id', 'timestamp'}
+    assert set(load_result[1].keys()) == {'id', '_id', 'timestamp'}
+    storage.close()
+    assert not storage.memory
+
+
+def test_file_storage(testdir_file_storage, dummy_deliveries):
+    storage = FileStorage(item_type=Delivery, key='id', path=testdir_file_storage)
+    assert storage.key == 'id'
+    storage.save(item=dummy_deliveries)
+    assert sorted(os.listdir(testdir_file_storage)) == sorted(['%s.json' % d.id for d in dummy_deliveries])
+    with pytest.raises(ValueError):
+        storage.save(item=dummy_deliveries[0])
+    assert dummy_deliveries[0].item == 'x'
+    dummy_deliveries[0].send(item='z')
+    storage.save(item=dummy_deliveries[0], update=True)
+    storage.remove(key=[dummy_deliveries[1].id, dummy_deliveries[2].id])
+    assert storage.exist(key=[d.id for d in dummy_deliveries]) == [True, False, False]
+    assert storage.exist(key=[d.id for d in dummy_deliveries], summary='and') is False
+    assert storage.exist(key=[d.id for d in dummy_deliveries], summary='or') is True
+    storage.save(item=[dummy_deliveries[1], dummy_deliveries[2]])
+    assert storage.exist(key=[d.id for d in dummy_deliveries]) == [True, True, True]
+    assert storage.exist(key=[d.id for d in dummy_deliveries], summary='and') is True
+    assert storage.exist(key=[d.id for d in dummy_deliveries], summary='or') is True
+    search_result = storage.search(key='item', value=json.dumps('?'))
+    assert type(search_result) == list and len(search_result) == 0
+    ref = sorted([dummy_deliveries[1].id, dummy_deliveries[2].id])
+    search_result = storage.search(key='item', value=json.dumps('z'))
+    assert len(search_result) == 1 and search_result[0].id == dummy_deliveries[0].id
+    search_result = storage.search(key='item', value=json.dumps('y'))
+    assert len(search_result) == 2
+    assert sorted([s.id for s in search_result]) == ref
+    search_result = storage.search(key='item', value=json.dumps('y'), to_dict=True)
+    assert len(search_result) == 2
+    assert sorted([s['id'] for s in search_result]) == ref
+    assert type(search_result[0]) == dict and type(search_result[1]) == dict
+    assert set(search_result[0].keys()) == {'item', 'id', '_id', 'timestamp', 'serial_number'}
+    assert set(search_result[1].keys()) == {'item', 'id', '_id', 'timestamp', 'serial_number'}
+    search_result = storage.search(key='item', value=json.dumps('y'), projection=['item'])
+    assert len(search_result) == 2
+    assert type(search_result[0]) == dict and type(search_result[1]) == dict
+    assert set(search_result[0].keys()) == {'item', 'id'}
+    assert set(search_result[1].keys()) == {'item', 'id'}
+    load_result = storage.load(key='???')
+    assert load_result is None
+    load_result = storage.load(key=['!??', '?!?', '??!'])
+    assert type(load_result) == list and len(load_result) == 0
+    load_result = storage.load(key=[d.id for d in dummy_deliveries])
+    assert type(load_result) == list and len(load_result) == len(dummy_deliveries)
+    for i in range(len(load_result)):
+        assert load_result[i].id == dummy_deliveries[i].id
+        assert load_result[i].item == dummy_deliveries[i].item
+        assert load_result[i].serial_number == dummy_deliveries[i].serial_number
+    load_result = storage.load(key=dummy_deliveries[1].id)
+    assert isinstance(load_result, Delivery)
+    load_result = storage.load(key=[dummy_deliveries[1].id, dummy_deliveries[2].id, '???'], to_dict=True)
+    assert type(load_result) == list and len(load_result) == 2
+    assert type(load_result[0]) == dict and type(load_result[1]) == dict
+    assert set(load_result[0].keys()) == {'item', 'id', '_id', 'timestamp', 'serial_number'}
+    assert set(load_result[1].keys()) == {'item', 'id', '_id', 'timestamp', 'serial_number'}
+    load_result = storage.load(key=[dummy_deliveries[1].id, dummy_deliveries[2].id], projection=['timestamp', '_id'])
+    assert type(load_result) == list
+    assert type(load_result[0]) == dict and type(load_result[1]) == dict
+    assert set(load_result[0].keys()) == {'id', '_id', 'timestamp'}
+    assert set(load_result[1].keys()) == {'id', '_id', 'timestamp'}
+    storage.new_path = True
+    storage.close()
+    assert not os.path.exists(testdir_file_storage)
+
+
+def test_mongo_storage(fake_mongodb, dummy_deliveries):
+    test_db = 'test'
+    test_collection = 'storage'
+    storage = MongoStorage(item_type=Delivery, key='id', mongodb=fake_mongodb, db=test_db, collection=test_collection)
+    assert storage.key == 'id'
+    storage.save(item=dummy_deliveries)
+    tmp = sorted([d['id'] for d in storage.mongodb[test_db][test_collection].find()])
+    assert tmp == sorted([d.id for d in dummy_deliveries])
+    with pytest.raises(ValueError):
+        storage.save(item=dummy_deliveries[0])
+    assert dummy_deliveries[0].item == 'x'
+    dummy_deliveries[0].send(item='z')
+    storage.save(item=dummy_deliveries[0], update=True)
+    storage.remove(key=[dummy_deliveries[1].id, dummy_deliveries[2].id])
+    assert storage.exist(key=[d.id for d in dummy_deliveries]) == [True, False, False]
+    assert storage.exist(key=[d.id for d in dummy_deliveries], summary='and') is False
+    assert storage.exist(key=[d.id for d in dummy_deliveries], summary='or') is True
+    storage.save(item=[dummy_deliveries[1], dummy_deliveries[2]])
+    assert storage.exist(key=[d.id for d in dummy_deliveries]) == [True, True, True]
+    assert storage.exist(key=[d.id for d in dummy_deliveries], summary='and') is True
+    assert storage.exist(key=[d.id for d in dummy_deliveries], summary='or') is True
+    search_result = storage.search(key='item', value=json.dumps('?'))
+    assert type(search_result) == list and len(search_result) == 0
+    ref = sorted([dummy_deliveries[1].id, dummy_deliveries[2].id])
+    search_result = storage.search(key='item', value=json.dumps('z'))
+    assert len(search_result) == 1 and search_result[0].id == dummy_deliveries[0].id
+    search_result = storage.search(key='item', value=json.dumps('y'))
+    assert len(search_result) == 2
+    assert sorted([s.id for s in search_result]) == ref
+    search_result = storage.search(key='item', value=json.dumps('y'), to_dict=True)
+    assert len(search_result) == 2
+    assert sorted([s['id'] for s in search_result]) == ref
+    assert type(search_result[0]) == dict and type(search_result[1]) == dict
+    assert set(search_result[0].keys()) == {'item', 'id', '_id', 'timestamp', 'serial_number'}
+    assert set(search_result[1].keys()) == {'item', 'id', '_id', 'timestamp', 'serial_number'}
+    search_result = storage.search(key='item', value=json.dumps('y'), projection=['item'])
+    assert len(search_result) == 2
+    assert type(search_result[0]) == dict and type(search_result[1]) == dict
+    assert set(search_result[0].keys()) == {'item', 'id'}
+    assert set(search_result[1].keys()) == {'item', 'id'}
+    load_result = storage.load(key='???')
+    assert load_result is None
+    load_result = storage.load(key=['!??', '?!?', '??!'])
+    assert type(load_result) == list and len(load_result) == 0
+    load_result = storage.load(key=[d.id for d in dummy_deliveries])
+    assert type(load_result) == list and len(load_result) == len(dummy_deliveries)
+    for i in range(len(load_result)):
+        assert load_result[i].id == dummy_deliveries[i].id
+        assert load_result[i].item == dummy_deliveries[i].item
+        assert load_result[i].serial_number == dummy_deliveries[i].serial_number
+    load_result = storage.load(key=dummy_deliveries[1].id)
+    assert isinstance(load_result, Delivery)
+    load_result = storage.load(key=[dummy_deliveries[1].id, dummy_deliveries[2].id, '???'], to_dict=True)
+    assert type(load_result) == list and len(load_result) == 2
+    assert type(load_result[0]) == dict and type(load_result[1]) == dict
+    assert set(load_result[0].keys()) == {'item', 'id', '_id', 'timestamp', 'serial_number'}
+    assert set(load_result[1].keys()) == {'item', 'id', '_id', 'timestamp', 'serial_number'}
+    load_result = storage.load(key=[dummy_deliveries[1].id, dummy_deliveries[2].id], projection=['timestamp', '_id'])
+    assert type(load_result) == list
+    assert type(load_result[0]) == dict and type(load_result[1]) == dict
+    assert set(load_result[0].keys()) == {'id', '_id', 'timestamp'}
+    assert set(load_result[1].keys()) == {'id', '_id', 'timestamp'}
+    assert storage.new_connection is False
+    tmp = storage.mongodb
+    storage.close()
+    assert not tmp.closed()
+    assert storage.mongodb is None
+
+
+@patch('boto3.client')
+def test_s3_storage(mock_client, dummy_deliveries):
+    config = Config()
+    s3_config = config.get_config('s3')
+    ss = S3Storage(s3=s3_config, item_type=Delivery, key='id')
+    assert ss.s3.session is mock_client()
+    ss.close()
+    assert not ss.s3
diff --git a/tests/test_alias.py b/tests/test_alias.py
deleted file mode 100644
index 7fb62ce..0000000
--- a/tests/test_alias.py
+++ /dev/null
@@ -1,81 +0,0 @@
-from codepack.config import Alias
-import os
-import pytest
-from codepack.service import MemoryStorageService, FileStorageService
-
-
-def test_alias_from_individual_os_env():
-    a = Alias()
-    assert a.aliases is None
-    with pytest.raises(AttributeError):
-        tmp = a['memory_storage_service']
-    os_env = 'CODEPACK_ALIAS_MEMORY_STORAGE_SERVICE'
-    os.environ[os_env] = 'codepack.service.storage_service.memory_storage_service.MemoryStorageService'
-    assert a['memory_storage_service'] == MemoryStorageService
-    os.environ.pop(os_env)
-
-
-def test_alias_from_alias_path_os_env():
-    a = Alias()
-    assert a.aliases is None
-    with pytest.raises(AttributeError):
-        tmp = a['memory_storage_service']
-    os_env = 'CODEPACK_ALIAS_PATH'
-    os.environ[os_env] = 'config/alias.ini'
-    assert a['memory_storage_service'] == MemoryStorageService
-    os.environ.pop(os_env)
-
-
-def test_alias_from_config_path_os_env():
-    a = Alias()
-    assert a.aliases is None
-    with pytest.raises(AttributeError):
-        tmp = a['memory_storage_service']
-    os_env = 'CODEPACK_CONFIG_PATH'
-    os.environ[os_env] = 'config/test.ini'
-    assert a['memory_storage_service'] == MemoryStorageService
-    os.environ.pop(os_env)
-
-
-def test_alias_priority1():
-    a = Alias(data={'memory_storage_service': 'codepack.service.storage_service.file_storage_service.FileStorageService'})
-    assert a.aliases is not None
-    assert a['memory_storage_service'] == FileStorageService
-    os_env = 'CODEPACK_ALIAS_MEMORY_STORAGE_SERVICE'
-    os.environ[os_env] = 'codepack.service.storage_service.memory_storage_service.MemoryStorageService'
-    assert a['memory_storage_service'] == FileStorageService
-    os.environ.pop(os_env)
-    os_env = 'CODEPACK_ALIAS_PATH'
-    os.environ[os_env] = 'config/alias.ini'
-    assert a['memory_storage_service'] == FileStorageService
-    os.environ.pop(os_env)
-    os_env = 'CODEPACK_CONFIG_PATH'
-    os.environ[os_env] = 'config/test.ini'
-    assert a['memory_storage_service'] == FileStorageService
-    os.environ.pop(os_env)
-
-
-def test_alias_priority2():
-    a = Alias()
-    assert a.aliases is None
-    with pytest.raises(AttributeError):
-        tmp = a['memory_storage_service']
-    first_os_env = 'CODEPACK_ALIAS_MEMORY_STORAGE_SERVICE'
-    os.environ[first_os_env] = 'codepack.service.storage_service.file_storage_service.FileStorageService'
-    assert a['memory_storage_service'] == FileStorageService
-    os_env = 'CODEPACK_ALIAS_PATH'
-    os.environ[os_env] = 'config/alias.ini'
-    assert a['memory_storage_service'] == FileStorageService
-    os.environ.pop(os_env)
-    os_env = 'CODEPACK_CONFIG_PATH'
-    os.environ[os_env] = 'config/test.ini'
-    assert a['memory_storage_service'] == FileStorageService
-    os.environ.pop(os_env)
-    os.environ.pop(first_os_env)
-
-
-def test_alias_path_argument():
-    a = Alias(data='config/alias.ini')
-    assert a['memory_storage_service'] == MemoryStorageService
-    b = Alias(data='config/test.ini')
-    assert b['memory_storage_service'] == MemoryStorageService
diff --git a/tests/test_argpack.py b/tests/test_argpack.py
index 37d8451..d5b15e9 100644
--- a/tests/test_argpack.py
+++ b/tests/test_argpack.py
@@ -1,5 +1,4 @@
-from codepack import Code, CodePack
-from codepack.snapshot import CodePackSnapshot
+from codepack import Code, CodePack, CodePackSnapshot
 from tests import *
 import pytest
 
diff --git a/tests/test_code.py b/tests/test_code.py
index 8d9c9e1..37e180e 100644
--- a/tests/test_code.py
+++ b/tests/test_code.py
@@ -16,14 +16,33 @@ def test_print_args(default_os_env):
 
 
 def test_print_info(default_os_env):
-    code1 = Code(add2)
-    code2 = Code(add3)
+    code1 = Code(add2, env='test-env')
+    code2 = Code(add3, image='test-image', owner='admin')
     code1 >> code2
     code2.receive('b') << code1
-    assert code1.get_info() == "Code(id: add2, function: add2, args: (a, b), receive: {}, state: UNKNOWN)"
-    assert code2.get_info() == "Code(id: add3, function: add3, args: (a, b, c=2), receive: {'b': 'add2'}, state: UNKNOWN)"
-    assert code1.get_info(state=False) == "Code(id: add2, function: add2, args: (a, b), receive: {})"
-    assert code2.get_info(state=False) == "Code(id: add3, function: add3, args: (a, b, c=2), receive: {'b': 'add2'})"
+    assert code1.get_info() == "Code(id: add2, function: add2, args: (a, b), receive: {}," \
+                               " env: test-env, state: UNKNOWN)"
+    assert code2.get_info() == "Code(id: add3, function: add3, args: (a, b, c=2), receive: {'b': 'add2'}," \
+                               " image: test-image, owner: admin, state: UNKNOWN)"
+    assert code1.get_info(state=False) == "Code(id: add2, function: add2, args: (a, b), receive: {}, env: test-env)"
+    assert code2.get_info(state=False) == "Code(id: add3, function: add3, args: (a, b, c=2), receive: {'b': 'add2'}," \
+                                          " image: test-image, owner: admin)"
+    code1.image = 'test-image2'
+    code1.owner = 'admin2'
+    assert code1.get_info() == "Code(id: add2, function: add2, args: (a, b), receive: {}," \
+                               " env: test-env, image: test-image2, owner: admin2, state: UNKNOWN)"
+    assert code1.get_info(state=False) == "Code(id: add2, function: add2, args: (a, b), receive: {}," \
+                                          " env: test-env, image: test-image2, owner: admin2)"
+    code2.owner = None
+    assert code2.get_info() == "Code(id: add3, function: add3, args: (a, b, c=2), receive: {'b': 'add2'}," \
+                               " image: test-image, state: UNKNOWN)"
+    assert code2.get_info(state=False) == "Code(id: add3, function: add3, args: (a, b, c=2), receive: {'b': 'add2'}," \
+                                          " image: test-image)"
+    code2.image = None
+    assert code2.get_info() == "Code(id: add3, function: add3, args: (a, b, c=2), receive: {'b': 'add2'}," \
+                               " state: UNKNOWN)"
+    assert code2.get_info(
+        state=False) == "Code(id: add3, function: add3, args: (a, b, c=2), receive: {'b': 'add2'})"
 
 
 def test_get_function_from_source(default_os_env):
@@ -149,7 +168,7 @@ def test_dependency_error_propagation(default_os_env):
     assert code1.get_state() == 'ERROR'
     ret = code3(1, 2, 3)
     assert ret is None
-    assert code3.get_state() == 'ERROR'
+    assert code3.get_state() == 'WAITING'
 
 
 def test_validate_dependency_result(default_os_env):
@@ -165,29 +184,25 @@ def test_validate_dependency_result(default_os_env):
     code2.service['delivery'].cancel(code2.serial_number)
     ret = code3(a=3)
     assert ret is None
-    assert code3.get_state() == 'WAITING'
+    assert code3.get_state() == 'ERROR'
     code2(3, 4)
     code2.send_result(item=1, timestamp=datetime.now().timestamp() + 1)
     ret = code3(a=3)
-    assert ret is None
-    assert code3.get_state() == 'WAITING'
+    assert ret is 7
+    assert code3.get_state() == 'TERMINATED'
     code2(3, 4)
     snapshots = code3.dependency.load_snapshot()
-    deliveries = code3.dependency.check_delivery()
     assert len(snapshots) == 2
-    assert len(deliveries) == 2
+    assert code3.dependency.check_delivery() is True
     snapshot_dict = {x['_id']: x for x in snapshots}
     code2_state_info = snapshot_dict.pop(code2.serial_number)
-    assert code3.dependency.validate_delivery(snapshot=snapshot_dict.values(), delivery=deliveries) == 'PENDING'
-    update_time = code2_state_info.pop('timestamp')
+    assert code3.dependency.validate(snapshot=snapshot_dict.values()) == 'WAITING'
     snapshot_dict[code2.serial_number] = code2_state_info
-    assert code3.dependency.validate_delivery(snapshot=snapshot_dict.values(), delivery=deliveries) == 'PENDING'
-    snapshot_dict[code2.serial_number]['timestamp'] = update_time
-    delivery_dict = {x['_id']: x for x in deliveries}
-    send_time = delivery_dict[code2.serial_number].pop('timestamp')
-    assert code3.dependency.validate_delivery(snapshot=snapshot_dict.values(), delivery=delivery_dict.values()) == 'PENDING'
-    delivery_dict[code2.serial_number]['timestamp'] = send_time
-    assert code3.dependency.validate_delivery(snapshot=snapshot_dict.values(), delivery=delivery_dict.values()) == 'RESOLVED'
+    assert code3.dependency.validate(snapshot=snapshot_dict.values()) == 'READY'
+    code2.service['delivery'].cancel(code2.serial_number)
+    assert code3.dependency.validate(snapshot=snapshot_dict.values()) == 'ERROR'
+    code2.service['delivery'].send(id='dummy', serial_number=code2.serial_number, item=123)
+    assert code3.dependency.validate(snapshot=snapshot_dict.values()) == 'READY'
 
 
 def test_default_arg(default_os_env):
diff --git a/tests/test_config.py b/tests/test_config.py
deleted file mode 100644
index a499da8..0000000
--- a/tests/test_config.py
+++ /dev/null
@@ -1,225 +0,0 @@
-from codepack import Code
-from tests import *
-from codepack.config import Config
-import pytest
-import os
-from codepack.service import MemoryDeliveryService, FileSnapshotService, MongoStorageService
-
-
-def test_no_config():
-    with pytest.raises(AssertionError):
-        Code(add2)
-
-
-def test_default_config_with_os_env(default_os_env):
-    code = Code(add2)
-    assert code(1, 2) == 3
-
-
-def test_config_path():
-    code = Code(add2, config_path='config/test.ini')
-    assert code(1, 2) == 3
-
-
-def test_default_memory_code_snapshot_service_with_os_env(default):
-    config = Config()
-    with pytest.raises(AssertionError):
-        config.get_storage_config('code_snapshot')
-    env_source = 'CODEPACK_CODE_SNAPSHOT_SOURCE'
-    try:
-        os.environ[env_source] = 'memory'
-        mss = default.get_storage_instance('code_snapshot', 'snapshot_service')
-        assert hasattr(mss, 'memory')
-    finally:
-        if env_source in os.environ:
-            os.environ.pop(env_source, None)
-
-
-def test_default_file_code_snapshot_service_with_os_env(default):
-    config = Config()
-    with pytest.raises(AssertionError):
-        config.get_storage_config('code_snapshot')
-    env_source = 'CODEPACK_CODE_SNAPSHOT_SOURCE'
-    env_path = 'CODEPACK_CODE_SNAPSHOT_PATH'
-    try:
-        os.environ[env_source] = 'file'
-        os.environ[env_path] = 'tmp/'
-        fss = default.get_storage_instance('code_snapshot', 'snapshot_service')
-        assert hasattr(fss, 'path')
-        assert fss.path == 'tmp/'
-    finally:
-        for env in [env_source, env_path]:
-            if env in os.environ:
-                os.environ.pop(env, None)
-
-
-def test_default_mongo_code_snapshot_service_with_os_env(default):
-    config = Config()
-    with pytest.raises(AssertionError):
-        config.get_storage_config('code_snapshot')
-    env_source = 'CODEPACK_CODE_SNAPSHOT_SOURCE'
-    env_db = 'CODEPACK_CODE_SNAPSHOT_DB'
-    env_collection = 'CODEPACK_CODE_SNAPSHOT_COLLECTION'
-    env_config_path = 'CODEPACK_CONN_PATH'
-    mss = None
-    try:
-        os.environ[env_source] = 'mongodb'
-        os.environ[env_db] = 'test'
-        os.environ[env_collection] = 'snapshot'
-        os.environ[env_config_path] = 'config/test_conn.ini'
-        mss = default.get_storage_instance('code_snapshot', 'snapshot_service')
-        assert hasattr(mss, 'mongodb')
-    finally:
-        for env in [env_source, env_db, env_collection, env_config_path]:
-            if env in os.environ:
-                os.environ.pop(env, None)
-        if mss is not None and not mss.mongodb.closed:
-            mss.mongodb.close()
-
-
-def test_default_memory_delivery_service_with_os_env(default):
-    config = Config()
-    with pytest.raises(AssertionError):
-        config.get_storage_config('delivery')
-    env_source = 'CODEPACK_DELIVERY_SOURCE'
-    try:
-        os.environ[env_source] = 'memory'
-        mds = default.get_storage_instance('delivery', 'delivery_service')
-        assert hasattr(mds, 'memory')
-    finally:
-        if env_source in os.environ:
-            os.environ.pop(env_source, None)
-
-
-def test_default_file_delivery_service_with_os_env(default):
-    config = Config()
-    with pytest.raises(AssertionError):
-        config.get_storage_config('delivery')
-    env_source = 'CODEPACK_DELIVERY_SOURCE'
-    env_path = 'CODEPACK_DELIVERY_PATH'
-    try:
-        os.environ[env_source] = 'file'
-        os.environ[env_path] = 'tmp/'
-        fds = default.get_storage_instance('delivery', 'delivery_service')
-        assert hasattr(fds, 'path')
-        assert fds.path == 'tmp/'
-    finally:
-        for env in [env_source, env_path]:
-            if env in os.environ:
-                os.environ.pop(env, None)
-
-
-def test_default_mongo_delivery_service_with_os_env(default):
-    config = Config()
-    with pytest.raises(AssertionError):
-        config.get_storage_config('delivery')
-    env_source = 'CODEPACK_DELIVERY_SOURCE'
-    env_db = 'CODEPACK_DELIVERY_DB'
-    env_collection = 'CODEPACK_DELIVERY_COLLECTION'
-    env_config_path = 'CODEPACK_CONN_PATH'
-    mds = None
-    try:
-        os.environ[env_source] = 'mongodb'
-        os.environ[env_db] = 'test'
-        os.environ[env_collection] = 'delivery'
-        os.environ[env_config_path] = 'config/test_conn.ini'
-        mds = default.get_storage_instance('delivery', 'delivery_service')
-        assert hasattr(mds, 'mongodb')
-    finally:
-        for env in [env_source, env_db, env_collection, env_config_path]:
-            if env in os.environ:
-                os.environ.pop(env, None)
-        if mds is not None and not mds.mongodb.closed:
-            mds.mongodb.close()
-
-
-def test_default_memory_code_storage_service_with_os_env(default):
-    config = Config()
-    with pytest.raises(AssertionError):
-        config.get_storage_config('code')
-    env_source = 'CODEPACK_CODE_SOURCE'
-    try:
-        os.environ[env_source] = 'memory'
-        mss = default.get_storage_instance('code', 'storage_service')
-        assert hasattr(mss, 'memory')
-        assert mss.item_type == Code
-    finally:
-        if env_source in os.environ:
-            os.environ.pop(env_source, None)
-
-
-def test_default_file_code_storage_service_with_os_env(default):
-    config = Config()
-    with pytest.raises(AssertionError):
-        config.get_storage_config('code')
-    env_source = 'CODEPACK_CODE_SOURCE'
-    env_path = 'CODEPACK_CODE_PATH'
-    try:
-        os.environ[env_source] = 'file'
-        os.environ[env_path] = 'tmp/'
-        fss = default.get_storage_instance('code', 'storage_service')
-        assert hasattr(fss, 'path')
-        assert fss.path == 'tmp/'
-        assert fss.item_type == Code
-    finally:
-        for env in [env_source, env_path]:
-            if env in os.environ:
-                os.environ.pop(env, None)
-
-
-def test_default_mongo_code_storage_service_with_os_env(default):
-    config = Config()
-    with pytest.raises(AssertionError):
-        config.get_storage_config('code')
-    env_source = 'CODEPACK_CODE_SOURCE'
-    env_db = 'CODEPACK_CODE_DB'
-    env_collection = 'CODEPACK_CODE_COLLECTION'
-    env_config_path = 'CODEPACK_CONN_PATH'
-    mss = None
-    try:
-        os.environ[env_source] = 'mongodb'
-        os.environ[env_db] = 'test'
-        os.environ[env_collection] = 'codes'
-        os.environ[env_config_path] = 'config/test_conn.ini'
-        mss = default.get_storage_instance('code', 'storage_service')
-        assert hasattr(mss, 'mongodb')
-        assert mss.item_type == Code
-    finally:
-        for env in [env_source, env_db, env_collection, env_config_path]:
-            if env in os.environ:
-                os.environ.pop(env, None)
-        if mss is not None and not mss.mongodb.closed:
-            mss.mongodb.close()
-
-
-def test_if_default_services_have_single_instance_for_each_service(default, testdir_snapshot_service):
-    os.environ['CODEPACK_DELIVERY_SOURCE'] = 'memory'
-    os.environ['CODEPACK_CODE_SNAPSHOT_SOURCE'] = 'file'
-    os.environ['CODEPACK_CODE_SNAPSHOT_PATH'] = testdir_snapshot_service
-    os.environ['CODEPACK_CODE_SOURCE'] = 'mongodb'
-    os.environ['CODEPACK_CODE_DB'] = 'test'
-    os.environ['CODEPACK_CODE_COLLECTION'] = 'codes'
-    os.environ['CODEPACK_CONN_PATH'] = 'config/test_conn.ini'
-    try:
-        code1 = Code(add2)
-        code2 = Code(add3)
-        assert isinstance(code1.service['delivery'], MemoryDeliveryService)
-        assert isinstance(code1.service['snapshot'], FileSnapshotService)
-        assert isinstance(code1.service['storage'], MongoStorageService)
-        assert isinstance(code2.service['delivery'], MemoryDeliveryService)
-        assert isinstance(code2.service['snapshot'], FileSnapshotService)
-        assert isinstance(code2.service['storage'], MongoStorageService)
-
-        assert (code1.service['delivery']) == (code2.service['delivery'])
-        assert (code1.service['snapshot']) == (code2.service['snapshot'])
-        assert (code1.service['storage']) == (code2.service['storage'])
-
-        assert id(code1.service['delivery']) == id(code2.service['delivery'])
-        assert id(code1.service['snapshot']) == id(code2.service['snapshot'])
-        assert id(code1.service['storage']) == id(code2.service['storage'])
-    finally:
-        for env in ['CODEPACK_DELIVERY_SOURCE',
-                    'CODEPACK_CODE_SNAPSHOT_SOURCE', 'CODEPACK_CODE_SNAPSHOT_PATH',
-                    'CODEPACK_CODE_SOURCE', 'CODEPACK_CODE_DB', 'CODEPACK_CODE_COLLECTION',
-                    'CODEPACK_CONN_PATH']:
-            os.environ.pop(env, None)
diff --git a/tests/test_dependency.py b/tests/test_dependency.py
index 8b038e3..3ce80e3 100644
--- a/tests/test_dependency.py
+++ b/tests/test_dependency.py
@@ -1,4 +1,4 @@
-from codepack.dependency import Dependency
+from codepack import Dependency
 
 
 def test_eq():
diff --git a/tests/test_engine.py b/tests/test_dependency_monitor.py
similarity index 82%
rename from tests/test_engine.py
rename to tests/test_dependency_monitor.py
index 3acd389..5ac7091 100644
--- a/tests/test_engine.py
+++ b/tests/test_dependency_monitor.py
@@ -1,6 +1,5 @@
-from codepack import Code
+from codepack import Code, DependencyMonitor
 from tests import *
-from codepack.engine import BlockingEngine, NonBlockingEngine
 import time
 
 
@@ -15,8 +14,8 @@ def load_and_run_code_from_snapshot_and_raise_exception(snapshot):
     raise KeyboardInterrupt
 
 
-def test_blocking_engine(default_os_env):
-    be = BlockingEngine(callback=load_and_run_code_from_snapshot_and_raise_exception, interval=0.5)
+def test_dependency_monitor(default_os_env):
+    dm = DependencyMonitor(callback=load_and_run_code_from_snapshot_and_raise_exception, interval=0.5, background=False)
     c1 = Code(add3)
     c2 = Code(mul2)
     c3 = Code(combination)
@@ -39,22 +38,22 @@ def test_blocking_engine(default_os_env):
     assert c2.get_state() == 'TERMINATED'
     assert c3.get_state() == 'WAITING'
     assert c4.get_state() == 'WAITING'
-    be.start()
+    dm.start()
     assert c1.get_state() == 'TERMINATED'
     assert c2.get_state() == 'TERMINATED'
     assert c3.get_state() == 'TERMINATED'
     assert c4.get_state() == 'WAITING'
-    be.start()
+    dm.start()
     assert c1.get_state() == 'TERMINATED'
     assert c2.get_state() == 'TERMINATED'
     assert c3.get_state() == 'TERMINATED'
     assert c4.get_state() == 'TERMINATED'
     assert c4.get_result() == 114
-    be.stop()
+    dm.stop()
 
 
-def test_non_blocking_engine(default_os_env):
-    nbe = NonBlockingEngine(callback=load_and_run_code_from_snapshot, interval=0.5)
+def test_dependency_monitor_in_background(default_os_env):
+    dm = DependencyMonitor(callback=load_and_run_code_from_snapshot, interval=0.5, background=True)
     c1 = Code(add3)
     c2 = Code(mul2)
     c3 = Code(combination)
@@ -82,7 +81,7 @@ def test_non_blocking_engine(default_os_env):
     assert c3.get_state() == 'WAITING'
     assert c4.get_state() == 'WAITING'
     assert c5.get_state() == 'WAITING'
-    nbe.start()
+    dm.start()
     time.sleep(1)
     assert c1.get_state() == 'TERMINATED'
     assert c2.get_state() == 'TERMINATED'
@@ -90,4 +89,4 @@ def test_non_blocking_engine(default_os_env):
     assert c4.get_state() == 'TERMINATED'
     assert c5.get_state() == 'TERMINATED'
     assert c4.get_result() == 114
-    nbe.stop()
+    dm.stop()
diff --git a/tests/test_dependency_state.py b/tests/test_dependency_state.py
deleted file mode 100644
index 702b273..0000000
--- a/tests/test_dependency_state.py
+++ /dev/null
@@ -1,7 +0,0 @@
-from codepack.dependency import DependencyState
-
-
-def test_eq():
-    assert DependencyState.RESOLVED == 'RESOLVED'
-    assert DependencyState.RESOLVED != DependencyState.PENDING
-    assert DependencyState.RESOLVED == 0
diff --git a/tests/test_docker_manager.py b/tests/test_docker_manager.py
index 33ab556..443fc4d 100644
--- a/tests/test_docker_manager.py
+++ b/tests/test_docker_manager.py
@@ -1,4 +1,4 @@
-from codepack.docker import DockerManager
+from codepack import DockerManager
 
 
 def test_make_dockerfile(testdir_docker_manager):
@@ -21,7 +21,7 @@ def test_combine_requirements():
 
 def test_extract_requirements_from_file():
     requirements = DockerManager.extract_requirements_from_file('requirements.txt')
-    assert requirements == ['dill==0.3.4', 'pymongo==3.12.1', 'numpy',
+    assert requirements == ['dill==0.3.4', 'pymongo==3.12.1', 'numpy', 'pandas',
                             'sshtunnel==0.4.0', 'PyMySQL==1.0.2', 'pymssql==2.2.2',
                             'boto3==1.19.6', 'cx-Oracle==8.2.1', 'parse==1.19.0',
                             'APScheduler==3.8.1', 'kafka-python==2.0.2',
diff --git a/tests/test_interface.py b/tests/test_interface.py
deleted file mode 100644
index ed2ed49..0000000
--- a/tests/test_interface.py
+++ /dev/null
@@ -1,4 +0,0 @@
-"""
-from codepack.utils.config import get_config
-from codepack.interface import MongoDB
-"""
diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py
index a2de90d..5e488af 100644
--- a/tests/test_scheduler.py
+++ b/tests/test_scheduler.py
@@ -1,6 +1,27 @@
-from codepack import Code, CodePack
-from codepack.scheduler import MongoScheduler
+from codepack import Code, CodePack, Default, Scheduler
+from codepack.storage.mongo_jobstore import MongoJobStore
+from unittest.mock import patch
 from tests import *
+import os
+
+
+@patch('pymongo.MongoClient')
+def test_get_default_mongo_scheduler(mock_client):
+    try:
+        os.environ['CODEPACK_CONFIG_DIR'] = 'config'
+        os.environ['CODEPACK_CONFIG_PATH'] = 'test.ini'
+        scheduler = Default.get_scheduler()
+        assert isinstance(scheduler, Scheduler)
+        assert 'codepack' in scheduler.jobstores
+        jobstore = scheduler.jobstores['codepack']
+        assert isinstance(jobstore, MongoJobStore)
+        assert scheduler.supervisor == 'http://localhost:8000'
+        mock_client.assert_called_once_with(host='localhost', port=27017)
+        mock_client().__getitem__.assert_called_once_with('codepack')
+        mock_client().__getitem__().__getitem__.assert_called_once_with('scheduler')
+    finally:
+        os.environ.pop('CODEPACK_CONN_DIR', None)
+        os.environ.pop('CODEPACK_CONN_PATH', None)
 
 
 def test_mongo_jobstore_codepack_snapshot(default_os_env, fake_mongodb):
@@ -11,7 +32,8 @@ def test_mongo_jobstore_codepack_snapshot(default_os_env, fake_mongodb):
     job_id = 'job_test'
     db = 'test'
     collection = 'scheduler'
-    scheduler = MongoScheduler(db=db, collection=collection, mongodb=fake_mongodb, blocking=False)
+    jobstore = MongoJobStore(mongodb=fake_mongodb, db=db, collection=collection)
+    scheduler = Scheduler(jobstore=jobstore, blocking=False)
     scheduler.add_codepack(job_id=job_id, codepack=codepack, argpack=argpack, trigger='interval', seconds=30)
     scheduler.start()
     assert fake_mongodb[db][collection].count_documents({'_id': codepack.id}) == 0
diff --git a/tests/test_snapshot.py b/tests/test_snapshot.py
index 809c0d8..a1d8ccb 100644
--- a/tests/test_snapshot.py
+++ b/tests/test_snapshot.py
@@ -1,6 +1,4 @@
-from codepack.snapshot import Snapshot
-from codepack.snapshot import CodeSnapshot
-from codepack import Code, CodePack
+from codepack import Code, CodePack, Snapshot, CodeSnapshot
 from tests import *
 from datetime import datetime
 import pytest
@@ -33,7 +31,7 @@ def test_snapshot_diff():
 def test_code_snapshot_to_dict_and_from_dict(default_os_env):
     code1 = Code(mul2)
     code2 = Code(add2)
-    code3 = Code(add3)
+    code3 = Code(add3, image='test-image')
     code1 >> code3
     code2 >> code3
     code3.receive('c') << code2
@@ -51,6 +49,12 @@ def test_code_snapshot_to_dict_and_from_dict(default_os_env):
         assert dependency['id'] == code3_dependency.id
         assert dependency['serial_number'] == code3_dependency.serial_number
         assert dependency['arg'] == code3_dependency.arg
+    assert 'image' in snapshot_dict1
+    assert snapshot_dict1['image'] is 'test-image'
+    assert 'env' in snapshot_dict1
+    assert snapshot_dict1['env'] is None
+    assert 'owner' in snapshot_dict1
+    assert snapshot_dict1['owner'] is None
     snapshot2 = CodeSnapshot.from_dict(snapshot_dict1)
     assert snapshot1.id == snapshot2.id
     assert snapshot1.serial_number == snapshot2.serial_number
@@ -59,6 +63,8 @@ def test_code_snapshot_to_dict_and_from_dict(default_os_env):
     assert snapshot1.kwargs == snapshot2.kwargs
     assert snapshot1.source == snapshot2.source
     assert snapshot1.dependency == snapshot2.dependency
+    assert snapshot1.image == snapshot2.image
+    assert snapshot1.owner == snapshot2.owner
 
 
 def test_code_snapshot_diff(default_os_env):
@@ -124,7 +130,11 @@ def test_codepack_to_snapshot_and_from_snapshot(default_os_env):
     assert ret is None
     assert cp1.get_state() == 'ERROR'
     snapshot1 = cp1.to_snapshot(argpack=argpack)
+    assert snapshot1.owner is None
+    snapshot1['owner'] = 'codepack'
+    assert snapshot1.owner == 'codepack'
     cp2 = CodePack.from_snapshot(snapshot1)
+    assert cp2.owner == 'codepack'
     assert cp1.id == cp2.id
     assert cp1.serial_number == cp2.serial_number
     assert cp1.get_state() == cp2.get_state()
@@ -136,6 +146,8 @@ def test_codepack_to_snapshot_and_from_snapshot(default_os_env):
     # assert cp1.get_structure() == cp2.get_structure()
     assert cp1.subscribe == cp2.subscribe
     assert set(cp1.codes.keys()) == set(cp2.codes.keys())
+    assert cp1.owner is None
+    assert cp2.owner == 'codepack'
     for code_id in cp1.codes.keys():
         code1 = cp1.codes[code_id]
         code2 = cp2.codes[code_id]
diff --git a/tests/test_state.py b/tests/test_state.py
index b3f680a..85c2d82 100644
--- a/tests/test_state.py
+++ b/tests/test_state.py
@@ -1,6 +1,5 @@
-from codepack import Code
+from codepack import Code, State, Snapshot
 from tests import *
-from codepack.snapshot import Snapshot, State
 
 
 def test_state_eq():