Repository: reagento/dishka

Issue:
Subgraphs
**Use case:**
`Interactor` depends on `StatisticDbGateway` and `MainDbGateway`. Each of them uses SQLAlchemy `Session` object, so they have similar init typehints. The difference is that `StatisticDbGateway` requires `Session` bound to *Clickhouse* `Engine`, while `MainDbGateway` binds to *postgresql* `Engine`. Again both engines have same type, but created differently.

To solve this you can replace `Session` with some new type (literally `NewType("PgSession", Session)`), but it requires modification of all code base. Imagine that you took those objects from external library and cannot modify it. 

Another solution is again to use `NewType`, but create factories for all objects instead of registrering classes as depedencies directly. This requries a lot of work.

**Proposed solution** is to split those objects in 3 groups:

**SubGraph1**: `StatisticDbGateway`, `Session`, `Engine`
**SubGraph2**: `MainDbGateway`, `Session`, `Engine`
**MainGraph**: `Interactor`, `SubGraph1`, `SubGraph2`

Each graph here uses only his factories to solve dependencies or requests specially attached subgraphs. So here, gateways can request Session and they get exactly that session which is declared in his subgraph.

**Requirements:**
1. (Sub)graphs can have subgraphs
2. All (Sub)graphs are synchronized by scopes
3. Subgraph cannot access depedencies from parent graph 
4. When looking for factories subgraphs are requested only if they are not found in current graph itself.

It is an open question whether (sub)graph is just a container or another thing

Discussion:

Patch:
diff --git a/docs/advanced/components.rst b/docs/advanced/components.rst
new file mode 100644
index 0000000..c8d82a9
--- /dev/null
+++ b/docs/advanced/components.rst
@@ -0,0 +1,111 @@
+Components and providers isolation
+****************************************
+
+Problem definition
+===========================
+
+As you know, container can be created from multiple providers, which are dynamically bound together. It allows you to reuse them or partially override in tests. It works well while you have different types across all provided objects. But what if there are some intersections. Let's talk about three situations:
+
+1. Only several types are used with different meaning within a monolithic app.
+2. Several parts of an application are developed them or less independently, while they used within same processing context
+3. You have a modular application with multiple bounded contexts
+
+**First situation** can appear when you have for-example multiple thread pools for different tasks or multiple database connections for different databases. While they have special meaning you distinguish them by creating new types
+
+.. code-block:: python
+
+    from typing import NewType
+
+    MainDbConnection = NewType("MainDbConnection", Connection)
+
+Once you have different types dishka can now understand which one is used in each place
+
+In the **third situation** you actually have mini-applications inside bigger one with their own scopes and event lifecycle. So just create multiple containers.
+
+The different thing is when you have a bunch of different types and you do not want or even cannot replace them with new types as in p.1. For this case we have a different concept - **components**.
+
+
+Component
+==============
+**Component** - is an isolated group of providers within the same container identified by a string. There is always a default component (``DEFAULT_COMPONENT=""``).
+
+Component is **set for the whole provider**, but single provider can be used in multiple components using ``.to_component(name)``.
+
+.. code-block:: python
+
+    # default component is used when not specified
+    provider0 = Provider()
+
+    class MyProvider(Provider):
+        # component can be set in class
+        component = "component_name"
+
+    provider1 = MyProvider()
+
+    # component can be set on instance creation
+    provider2 = MyProvider(component="other")
+
+    # same provider instance is casted to use with different component
+    provider3 = provider2.to_component("additional")
+
+    container = make_container(provider0, provider1, provider2, provider3)
+
+Components are **isolated**: provider cannot implicitly request an object from another component. In the following code ``MainProvider.foo`` requests integer value which is only provided in separate component.
+
+.. code-block:: python
+
+    class MainProvider(Provider):
+        # default component is used here
+
+        @provide(scope=Scope.APP)
+        def foo(self, a:int) -> float:
+            return a/10
+
+
+    class AdditionalProvider(Provider):
+        component = "X"
+
+        @provide(scope=Scope.APP)
+        def foo(self) -> int:
+            return 1
+
+    container = make_container(MainProvider(), AdditionalProvider())
+    # retrieve from component "X"
+    container.get(int, component="X")  # value 1 is returned
+    # retrieve from default component
+    container.get(float)  # raises NoFactoryError because int is in another component
+
+
+If the same type is provided in multiple components, it is searched only within the same component as its dependant, unless it is declared explicitly.
+
+Components can **link to each other**: each provider can add a component name when declaring a dependency by ``FromComponent`` type annotation.
+
+.. code-block:: python
+
+    from typing import Annotated
+    from dishka import FromComponent
+
+    class MainProvider(Provider):
+
+        @provide(scope=Scope.APP)
+        def foo(self, a:Annotated[int, FromComponent("X")]) -> float:
+            return a/10
+
+
+    class AdditionalProvider(Provider):
+        component = "X"
+
+        @provide(scope=Scope.APP)
+        def foo(self) -> int:
+            return 1
+
+    container = make_container(MainProvider(), AdditionalProvider())
+    container.get(float)  # returns 0.1
+
+
+``alias`` now can be used across components without changing the type:
+
+.. code-block:: python
+
+    a = alias(int, component="X")
+
diff --git a/docs/concepts.rst b/docs/concepts.rst
index ab578a9..f833275 100644
--- a/docs/concepts.rst
+++ b/docs/concepts.rst
@@ -89,3 +89,33 @@ There are 3 special functions:
 * ``@provide`` is used to declare a factory providing a dependency. It can be used with some class or as a method decorator. :ref:`Read more<provide>`
 * ``alias`` is used to allow retrieving of the same object by different type hints. :ref:`Read more<alias>`
 * ``decorate`` is used to modify or wrap an object which is already configured in another ``Provider``. :ref:`Read more<decorate>`
+
+Component
+====================
+**Component** - is an isolated group of providers within the same container identified by a string. When dependency is requested it is searched only within the same component as its dependant, unless it is declared explicitly.
+
+This allows you to have multiple parts of application build separately without need to think if the use same types.
+
+.. code-block:: python
+
+    class MainProvider(Provider):
+
+        @provide(scope=Scope.APP)
+        def foo(self, a:Annotated[int, FromComponent("X")]) -> float:
+            return a/10
+
+        @provide(scope=Scope.APP)
+        def bar(self, a:int) -> complex:
+            return a + 0j
+
+
+    class AdditionalProvider(Provider):
+        component = "X"
+
+        @provide(scope=Scope.APP)
+        def foo(self) -> int:
+            return 1
+
+    container = make_container(MainProvider(), AdditionalProvider())
+    container.get(float)  # returns 0.1
+    container.get(complex)  # raises NoFactoryError
\ No newline at end of file
diff --git a/docs/index.rst b/docs/index.rst
index 7f5c3b4..548270e 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -15,6 +15,8 @@ This library is targeting to provide only an IoC-container but make it really us
    provider/index
    container/index
    integrations/index
+   advanced/testing
+   advanced/components
    contributing
 
 .. toctree::
diff --git a/docs/integrations/index.rst b/docs/integrations/index.rst
index 66906df..381cbdd 100644
--- a/docs/integrations/index.rst
+++ b/docs/integrations/index.rst
@@ -3,4 +3,29 @@ Using with frameworks
 
 For several frameworks library contains helper functions so you don't need to control scopes yourself, but just annotate handler/view functions and change application startup code
 
-.. toctree::
+To use framework integration you mainly need to do 3 things:
+
+* call ``setup_dishka`` on your container and framework entity
+* add ``Annotated[YourClass, Depends()]`` on you framework handlers (or view-functions)
+* decorate your handlers with ``@inject`` before registering them in framework
+
+For FastAPI it will look like:
+
+.. code-block:: python
+
+   from dishka.integrations.fastapi import Depends, inject, setup_dishka
+
+   @router.get("/")
+   @inject
+   async def index(interactor: Annotated[Interactor, Depends()]) -> str:
+       result = interactor()
+       return result
+
+   app = FastAPI(lifespan=lifespan)
+   container = make_async_container(provider)
+   setup_dishka(container, app)
+
+
+For such integrations library enters scope for each generated event. So if you have standard scope, than handler dependencies will be retrieved as for ``Scope.REQUEST``.
+
+Additionally, you may need to call ``container.close()`` in the end of your application lifecycle if you want to finalize APP-scoped dependencies
\ No newline at end of file
diff --git a/docs/requirements/technical.rst b/docs/requirements/technical.rst
index cedd6b9..07f2f9a 100644
--- a/docs/requirements/technical.rst
+++ b/docs/requirements/technical.rst
@@ -49,6 +49,8 @@ Technical requirements
 1. There can be multiple containers within same code base for different purposes
 2. There must be a way to assemble a container from some reusable parts.
 3. Assembling of container should be done in runtime in local scope
+4. There should be a way to isolate different parts of container so they do not affect each other
+5. Isolated parts should have API to interact with each other if it is requested explicitly
 
 7. Usability
 ================
diff --git a/examples/benchmarks/with_dishka.py b/examples/benchmarks/with_dishka.py
index 0011688..a674920 100644
--- a/examples/benchmarks/with_dishka.py
+++ b/examples/benchmarks/with_dishka.py
@@ -22,10 +22,10 @@ class MyProvider(Provider):
 
 
 def main():
-    with make_container(MyProvider(), scopes=MyScope) as container:
-        for x in range(NUMBER):
-            with container() as state:
-                state.get(A)
+    container =  make_container(MyProvider(), scopes=MyScope)
+    for x in range(NUMBER):
+        with container() as state:
+            state.get(A)
 
 
 if __name__ == '__main__':
diff --git a/src/dishka/__init__.py b/src/dishka/__init__.py
index 7972150..b51183b 100644
--- a/src/dishka/__init__.py
+++ b/src/dishka/__init__.py
@@ -1,13 +1,20 @@
 __all__ = [
     "make_async_container", "AsyncContainer",
-    "make_container", "Container",
+    "DEFAULT_COMPONENT", "Component",
+    "make_container", "Container", "FromComponent",
     "Provider",
-    "alias", "decorate", "provide",
+    "alias", "decorate", "provide", "DependencyKey",
     "BaseScope", "Scope",
 ]
 
 from .async_container import AsyncContainer, make_async_container
 from .container import Container, make_container
-from .dependency_source import alias, decorate, provide
+from .dependency_source import (
+    alias,
+    decorate,
+    provide,
+)
+from .entities.component import DEFAULT_COMPONENT, Component
+from .entities.key import DependencyKey, FromComponent
+from .entities.scope import BaseScope, Scope
 from .provider import Provider
-from .scope import BaseScope, Scope
diff --git a/src/dishka/async_container.py b/src/dishka/async_container.py
index 083f648..647408f 100644
--- a/src/dishka/async_container.py
+++ b/src/dishka/async_container.py
@@ -3,6 +3,9 @@ from collections.abc import Callable
 from dataclasses import dataclass
 from typing import Any, Optional, TypeVar
 
+from dishka.entities.component import DEFAULT_COMPONENT, Component
+from dishka.entities.key import DependencyKey
+from dishka.entities.scope import BaseScope, Scope
 from .dependency_source import Factory, FactoryType
 from .exceptions import (
     ExitError,
@@ -11,7 +14,6 @@ from .exceptions import (
 )
 from .provider import Provider
 from .registry import Registry, make_registries
-from .scope import BaseScope, Scope
 
 T = TypeVar("T")
 
@@ -39,9 +41,14 @@ class AsyncContainer:
     ):
         self.registry = registry
         self.child_registries = child_registries
-        self.context = {type(self): self}
+        self.context = {DependencyKey(type(self), DEFAULT_COMPONENT): self}
         if context:
-            self.context.update(context)
+            for key, value in context.items():
+                if isinstance(key, DependencyKey):
+                    self.context[key] = value
+                else:
+                    self.context[DependencyKey(key, DEFAULT_COMPONENT)] = value
+
         self.parent_container = parent_container
         if lock_factory:
             self.lock = lock_factory()
@@ -77,7 +84,7 @@ class AsyncContainer:
         return AsyncContextWrapper(self._create_child(context, lock_factory))
 
     async def _get_from_self(
-            self, factory: Factory, dependency_type: Any,
+            self, factory: Factory, key: DependencyKey,
     ) -> T:
         try:
             sub_dependencies = [
@@ -85,7 +92,7 @@ class AsyncContainer:
                 for dependency in factory.dependencies
             ]
         except NoFactoryError as e:
-            e.add_path(dependency_type)
+            e.add_path(key)
             raise
 
         if factory.type is FactoryType.GENERATOR:
@@ -107,25 +114,32 @@ class AsyncContainer:
                 f"Unsupported factory type {factory.type}.",
             )
         if factory.cache:
-            self.context[dependency_type] = solved
+            self.context[key] = solved
         return solved
 
-    async def get(self, dependency_type: type[T]) -> T:
+    async def get(
+            self,
+            dependency_type: type[T],
+            component: Component = DEFAULT_COMPONENT,
+    ) -> T:
         lock = self.lock
+        key = DependencyKey(dependency_type, component)
         if not lock:
-            return await self._get_unlocked(dependency_type)
+            return await self._get_unlocked(key)
         async with lock:
-            return await self._get_unlocked(dependency_type)
+            return await self._get_unlocked(key)
 
-    async def _get_unlocked(self, dependency_type: type[T]) -> T:
-        if dependency_type in self.context:
-            return self.context[dependency_type]
-        factory = self.registry.get_factory(dependency_type)
+    async def _get_unlocked(self, key: DependencyKey) -> Any:
+        if key in self.context:
+            return self.context[key]
+        factory = self.registry.get_factory(key)
         if not factory:
             if not self.parent_container:
-                raise NoFactoryError(dependency_type)
-            return await self.parent_container.get(dependency_type)
-        return await self._get_from_self(factory, dependency_type)
+                raise NoFactoryError(key)
+            return await self.parent_container.get(
+                key.type_hint, key.component,
+            )
+        return await self._get_from_self(factory, key)
 
     async def close(self):
         errors = []
diff --git a/src/dishka/container.py b/src/dishka/container.py
index f05ec21..6c8dd88 100644
--- a/src/dishka/container.py
+++ b/src/dishka/container.py
@@ -3,6 +3,9 @@ from dataclasses import dataclass
 from threading import Lock
 from typing import Any, Optional, TypeVar
 
+from dishka.entities.component import DEFAULT_COMPONENT, Component
+from dishka.entities.key import DependencyKey
+from dishka.entities.scope import BaseScope, Scope
 from .dependency_source import Factory, FactoryType
 from .exceptions import (
     ExitError,
@@ -11,7 +14,6 @@ from .exceptions import (
 )
 from .provider import Provider
 from .registry import Registry, make_registries
-from .scope import BaseScope, Scope
 
 T = TypeVar("T")
 
@@ -39,9 +41,13 @@ class Container:
     ):
         self.registry = registry
         self.child_registries = child_registries
-        self.context = {type(self): self}
+        self.context = {DependencyKey(type(self), DEFAULT_COMPONENT): self}
         if context:
-            self.context.update(context)
+            for key, value in context.items():
+                if isinstance(key, DependencyKey):
+                    self.context[key] = value
+                else:
+                    self.context[DependencyKey(key, DEFAULT_COMPONENT)] = value
         self.parent_container = parent_container
         if lock_factory:
             self.lock = lock_factory()
@@ -77,7 +83,7 @@ class Container:
         return ContextWrapper(self._create_child(context, lock_factory))
 
     def _get_from_self(
-            self, factory: Factory, dependency_type: Any,
+            self, factory: Factory, key: DependencyKey,
     ) -> T:
         try:
             sub_dependencies = [
@@ -85,7 +91,7 @@ class Container:
                 for dependency in factory.dependencies
             ]
         except NoFactoryError as e:
-            e.add_path(dependency_type)
+            e.add_path(key)
             raise
 
         if factory.type is FactoryType.GENERATOR:
@@ -111,25 +117,32 @@ class Container:
                 f"Unsupported factory type {factory.type}. ",
             )
         if factory.cache:
-            self.context[dependency_type] = solved
+            self.context[key] = solved
         return solved
 
-    def get(self, dependency_type: type[T]) -> T:
+    def get(
+            self,
+            dependency_type: type[T],
+            component: Component = DEFAULT_COMPONENT,
+    ) -> T:
         lock = self.lock
+        key = DependencyKey(dependency_type, component)
         if not lock:
-            return self._get_unlocked(dependency_type)
+            return self._get_unlocked(key)
         with lock:
-            return self._get_unlocked(dependency_type)
+            return self._get_unlocked(key)
 
-    def _get_unlocked(self, dependency_type: type[T]) -> T:
-        if dependency_type in self.context:
-            return self.context[dependency_type]
-        factory = self.registry.get_factory(dependency_type)
+    def _get_unlocked(self, key: DependencyKey) -> Any:
+        if key in self.context:
+            return self.context[key]
+        factory = self.registry.get_factory(key)
         if not factory:
             if not self.parent_container:
-                raise NoFactoryError(dependency_type)
-            return self.parent_container.get(dependency_type)
-        return self._get_from_self(factory, dependency_type)
+                raise NoFactoryError(key)
+            return self.parent_container.get(
+                key.type_hint, key.component,
+            )
+        return self._get_from_self(factory, key)
 
     def close(self) -> None:
         errors = []
diff --git a/src/dishka/dependency_source/__init__.py b/src/dishka/dependency_source/__init__.py
new file mode 100644
index 0000000..d206f83
--- /dev/null
+++ b/src/dishka/dependency_source/__init__.py
@@ -0,0 +1,12 @@
+__all__ = [
+    "DependencySource",
+    "alias", "Alias",
+    "decorate", "Decorator",
+    "provide", "Factory", "FactoryType",
+]
+
+from .alias import Alias, alias
+from .decorator import Decorator, decorate
+from .factory import Factory, FactoryType, provide
+
+DependencySource = Alias | Factory | Decorator
diff --git a/src/dishka/dependency_source/alias.py b/src/dishka/dependency_source/alias.py
new file mode 100644
index 0000000..f28c6a8
--- /dev/null
+++ b/src/dishka/dependency_source/alias.py
@@ -0,0 +1,63 @@
+from typing import (
+    Any,
+)
+
+from dishka.entities.component import Component
+from dishka.entities.key import DependencyKey
+from dishka.entities.scope import BaseScope
+from .factory import Factory, FactoryType
+
+
+def _identity(x: Any) -> Any:
+    return x
+
+
+class Alias:
+    __slots__ = ("source", "provides", "cache", "component")
+
+    def __init__(
+            self, *,
+            source: DependencyKey,
+            provides: DependencyKey,
+            cache: bool,
+    ) -> None:
+        self.source = source
+        self.provides = provides
+        self.cache = cache
+
+    def as_factory(
+            self, scope: BaseScope, component: Component,
+    ) -> Factory:
+        return Factory(
+            scope=scope,
+            source=_identity,
+            provides=self.provides.with_component(component),
+            is_to_bind=False,
+            dependencies=[self.source.with_component(component)],
+            type_=FactoryType.FACTORY,
+            cache=self.cache,
+        )
+
+    def __get__(self, instance, owner):
+        return self
+
+
+def alias(
+        *,
+        source: type,
+        provides: type | None = None,
+        cache: bool = True,
+        component: Component | None = None,
+) -> Alias:
+    if component is provides is None:
+        raise ValueError("Either component or provides must be set in alias")
+    if provides is None:
+        provides = source
+    return Alias(
+        source=DependencyKey(
+            type_hint=source,
+            component=component,
+        ),
+        provides=DependencyKey(provides, None),
+        cache=cache,
+    )
diff --git a/src/dishka/dependency_source/decorator.py b/src/dishka/dependency_source/decorator.py
new file mode 100644
index 0000000..44b3d0d
--- /dev/null
+++ b/src/dishka/dependency_source/decorator.py
@@ -0,0 +1,81 @@
+from collections.abc import (
+    Callable,
+)
+from typing import (
+    Any,
+    overload,
+)
+
+from dishka.entities.component import Component
+from dishka.entities.key import DependencyKey
+from dishka.entities.scope import BaseScope
+from .factory import Factory, make_factory
+
+
+class Decorator:
+    __slots__ = ("provides", "factory")
+
+    def __init__(self, factory: Factory):
+        self.factory = factory
+        self.provides = factory.provides
+
+    def as_factory(
+            self, *,
+            scope: BaseScope,
+            new_dependency: DependencyKey,
+            cache: bool,
+            component: Component,
+    ) -> Factory:
+        return Factory(
+            scope=scope,
+            source=self.factory.source,
+            provides=self.factory.provides.with_component(component),
+            is_to_bind=self.factory.is_to_bind,
+            dependencies=[
+                (
+                    new_dependency
+                    if dep.type_hint == self.provides.type_hint
+                    else dep
+                ).with_component(component)
+                for dep in self.factory.dependencies
+            ],
+            type_=self.factory.type,
+            cache=cache,
+        )
+
+    def __get__(self, instance, owner):
+        return Decorator(self.factory.__get__(instance, owner))
+
+
+@overload
+def decorate(
+        *,
+        provides: Any = None,
+) -> Callable[[Callable], Decorator]:
+    ...
+
+
+@overload
+def decorate(
+        source: Callable | type,
+        *,
+        provides: Any = None,
+) -> Decorator:
+    ...
+
+
+def decorate(
+        source: Callable | type | None = None,
+        provides: Any = None,
+) -> Decorator | Callable[[Callable], Decorator]:
+    if source is not None:
+        return Decorator(make_factory(
+            provides=provides, scope=None, source=source, cache=False,
+        ))
+
+    def scoped(func):
+        return Decorator(make_factory(
+            provides=provides, scope=None, source=func, cache=False,
+        ))
+
+    return scoped
diff --git a/src/dishka/dependency_source.py b/src/dishka/dependency_source/factory.py
similarity index 77%
rename from src/dishka/dependency_source.py
rename to src/dishka/dependency_source/factory.py
index c02f5e1..5d14589 100644
--- a/src/dishka/dependency_source.py
+++ b/src/dishka/dependency_source/factory.py
@@ -26,16 +26,18 @@ from typing import (
     overload,
 )
 
-from ._adaptix.type_tools.basic_utils import (
+from dishka._adaptix.type_tools.basic_utils import (
     get_all_type_hints,
     get_type_vars,
     is_bare_generic,
 )
-from ._adaptix.type_tools.generic_resolver import (
+from dishka._adaptix.type_tools.generic_resolver import (
     GenericResolver,
     MembersStorage,
 )
-from .scope import BaseScope
+from dishka.entities.component import Component
+from dishka.entities.key import DependencyKey, hints_to_dependency_keys
+from dishka.entities.scope import BaseScope
 
 
 class FactoryType(Enum):
@@ -50,10 +52,6 @@ def _is_bound_method(obj):
     return ismethod(obj) and obj.__self__
 
 
-def _identity(x: Any) -> Any:
-    return x
-
-
 class Factory:
     __slots__ = (
         "dependencies", "source", "provides", "scope", "type",
@@ -63,9 +61,9 @@ class Factory:
     def __init__(
             self,
             *,
-            dependencies: Sequence[Any],
+            dependencies: Sequence[DependencyKey],
             source: Any,
-            provides: type,
+            provides: DependencyKey,
             scope: BaseScope | None,
             type_: FactoryType,
             is_to_bind: bool,
@@ -99,6 +97,19 @@ class Factory:
             cache=self.cache,
         )
 
+    def with_component(self, component: Component) -> "Factory":
+        return Factory(
+            dependencies=[
+                d.with_component(component) for d in self.dependencies
+            ],
+            source=self.source,
+            provides=self.provides.with_component(component),
+            scope=self.scope,
+            is_to_bind=self.is_to_bind,
+            cache=self.cache,
+            type_=self.type,
+        )
+
 
 def _get_init_members(tp) -> MembersStorage[str, None]:
     type_hints = get_all_type_hints(tp.__init__)
@@ -181,11 +192,11 @@ def _make_factory_by_class(
     is_to_bind = False
     factory_type = FactoryType.FACTORY
     return Factory(
-        dependencies=dependencies,
+        dependencies=hints_to_dependency_keys(dependencies),
         type_=factory_type,
         source=source,
         scope=scope,
-        provides=provides,
+        provides=DependencyKey(provides, None),
         is_to_bind=is_to_bind,
         cache=cache,
     )
@@ -221,11 +232,11 @@ def _make_factory_by_method(
         provides = _clean_result_hint(factory_type, possible_dependency)
 
     return Factory(
-        dependencies=dependencies,
+        dependencies=hints_to_dependency_keys(dependencies),
         type_=factory_type,
         source=source,
         scope=scope,
-        provides=provides,
+        provides=DependencyKey(provides, None),
         is_to_bind=is_to_bind,
         cache=cache,
     )
@@ -244,14 +255,13 @@ def _make_factory_by_static_method(
     dependencies = list(hints.values())
     if not provides:
         provides = _clean_result_hint(factory_type, possible_dependency)
-    is_to_bind = False
     return Factory(
-        dependencies=dependencies,
+        dependencies=hints_to_dependency_keys(dependencies),
         type_=factory_type,
         source=source,
         scope=scope,
-        provides=provides,
-        is_to_bind=is_to_bind,
+        provides=DependencyKey(provides, None),
+        is_to_bind=False,
         cache=cache,
     )
 
@@ -273,20 +283,17 @@ def _make_factory_by_other_callable(
         cache=cache,
         scope=scope,
     )
-    factory_type = factory.type
     if factory.is_to_bind:
         dependencies = factory.dependencies[1:]  # remove `self`
     else:
         dependencies = factory.dependencies
-    provides = factory.provides
-    is_to_bind = False
     return Factory(
         dependencies=dependencies,
-        type_=factory_type,
+        type_=factory.type,
         source=source,
         scope=scope,
-        provides=provides,
-        is_to_bind=is_to_bind,
+        provides=factory.provides,
+        is_to_bind=False,
         cache=cache,
     )
 
@@ -380,103 +387,3 @@ def provide(
         )
 
     return scoped
-
-
-class Alias:
-    __slots__ = ("source", "provides", "cache")
-
-    def __init__(self, *, source, provides, cache: bool):
-        self.source = source
-        self.provides = provides
-        self.cache = cache
-
-    def as_factory(self, scope: BaseScope) -> Factory:
-        return Factory(
-            scope=scope,
-            source=_identity,
-            provides=self.provides,
-            is_to_bind=False,
-            dependencies=[self.source],
-            type_=FactoryType.FACTORY,
-            cache=self.cache,
-        )
-
-    def __get__(self, instance, owner):
-        return self
-
-
-def alias(
-        *,
-        source: type,
-        provides: type,
-        cache: bool = True,
-) -> Alias:
-    return Alias(
-        source=source,
-        provides=provides,
-        cache=cache,
-    )
-
-
-class Decorator:
-    __slots__ = ("provides", "factory")
-
-    def __init__(self, factory: Factory):
-        self.factory = factory
-        self.provides = factory.provides
-
-    def as_factory(
-            self, *, scope: BaseScope, new_dependency: Any, cache: bool,
-    ) -> Factory:
-        return Factory(
-            scope=scope,
-            source=self.factory.source,
-            provides=self.factory.provides,
-            is_to_bind=self.factory.is_to_bind,
-            dependencies=[
-                new_dependency if dep is self.provides else dep
-                for dep in self.factory.dependencies
-            ],
-            type_=self.factory.type,
-            cache=cache,
-        )
-
-    def __get__(self, instance, owner):
-        return Decorator(self.factory.__get__(instance, owner))
-
-
-@overload
-def decorate(
-        *,
-        provides: Any = None,
-) -> Callable[[Callable], Decorator]:
-    ...
-
-
-@overload
-def decorate(
-        source: Callable | type,
-        *,
-        provides: Any = None,
-) -> Decorator:
-    ...
-
-
-def decorate(
-        source: Callable | type | None = None,
-        provides: Any = None,
-) -> Decorator | Callable[[Callable], Decorator]:
-    if source is not None:
-        return Decorator(make_factory(
-            provides=provides, scope=None, source=source, cache=False,
-        ))
-
-    def scoped(func):
-        return Decorator(make_factory(
-            provides=provides, scope=None, source=func, cache=False,
-        ))
-
-    return scoped
-
-
-DependencySource = Alias | Factory | Decorator
diff --git a/src/dishka/entities/__init__.py b/src/dishka/entities/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/dishka/entities/component.py b/src/dishka/entities/component.py
new file mode 100644
index 0000000..fc1f4cd
--- /dev/null
+++ b/src/dishka/entities/component.py
@@ -0,0 +1,3 @@
+Component = str
+
+DEFAULT_COMPONENT = ""
diff --git a/src/dishka/entities/key.py b/src/dishka/entities/key.py
new file mode 100644
index 0000000..3117f32
--- /dev/null
+++ b/src/dishka/entities/key.py
@@ -0,0 +1,38 @@
+from typing import Annotated, Any, NamedTuple, get_args, get_origin
+
+from .component import DEFAULT_COMPONENT, Component
+
+
+class FromComponent(NamedTuple):
+    component: Component = DEFAULT_COMPONENT
+
+
+class DependencyKey(NamedTuple):
+    type_hint: Any
+    component: Component | None
+
+    def with_component(self, component: Component) -> "DependencyKey":
+        if self.component is not None:
+            return self
+        return DependencyKey(
+            type_hint=self.type_hint,
+            component=component,
+        )
+
+
+def hint_to_dependency_key(hint: Any) -> DependencyKey:
+    if get_origin(hint) is not Annotated:
+        return DependencyKey(hint, None)
+    args = get_args(hint)
+    from_component = next(
+        (arg for arg in args if isinstance(arg, FromComponent)),
+        None,
+    )
+    return DependencyKey(args[0], from_component.component)
+
+
+def hints_to_dependency_keys(hints: list) -> list[DependencyKey]:
+    return [
+        hint_to_dependency_key(type_hint)
+        for type_hint in hints
+    ]
diff --git a/src/dishka/scope.py b/src/dishka/entities/scope.py
similarity index 100%
rename from src/dishka/scope.py
rename to src/dishka/entities/scope.py
diff --git a/src/dishka/provider.py b/src/dishka/provider.py
index 72288f3..9c645bf 100644
--- a/src/dishka/provider.py
+++ b/src/dishka/provider.py
@@ -2,6 +2,8 @@ import inspect
 from collections.abc import Callable
 from typing import Any
 
+from dishka.entities.component import DEFAULT_COMPONENT, Component
+from dishka.entities.scope import BaseScope
 from .dependency_source import (
     Alias,
     Decorator,
@@ -12,7 +14,6 @@ from .dependency_source import (
     provide,
 )
 from .exceptions import InvalidGraphError
-from .scope import BaseScope
 
 
 def is_dependency_source(attribute: Any) -> bool:
@@ -33,13 +34,20 @@ class Provider:
     creating a container
     """
     scope: BaseScope | None = None
+    component: Component = DEFAULT_COMPONENT
 
-    def __init__(self, scope: BaseScope | None = None):
+    def __init__(
+            self,
+            scope: BaseScope | None = None,
+            component: Component | None = None,
+    ):
         self.factories: list[Factory] = []
         self.aliases: list[Alias] = []
         self.decorators: list[Decorator] = []
         self._init_dependency_sources()
         self.scope = self.scope or scope
+        if component is not None:
+            self.component = component
 
     def _init_dependency_sources(self) -> None:
         processed_types = {}
@@ -106,3 +114,13 @@ class Provider:
         )
         self.aliases.append(new_decorator)
         return new_decorator
+
+    def to_component(self, component: Component) -> "Provider":
+        provider = Provider(
+            scope=self.scope,
+            component=component,
+        )
+        provider.factories = self.factories
+        provider.aliases = self.aliases
+        provider.decorators = self.decorators
+        return provider
diff --git a/src/dishka/registry.py b/src/dishka/registry.py
index b7255a2..7f9ff0e 100644
--- a/src/dishka/registry.py
+++ b/src/dishka/registry.py
@@ -1,34 +1,38 @@
 from collections import defaultdict
 from typing import Any, NewType, TypeVar, get_args, get_origin
 
+from dishka.entities.key import DependencyKey
+from dishka.entities.scope import BaseScope
 from ._adaptix.type_tools.basic_utils import get_type_vars, is_generic
 from .dependency_source import Factory
 from .exceptions import InvalidGraphError
 from .provider import Provider
-from .scope import BaseScope
 
 
 class Registry:
     __slots__ = ("scope", "_factories")
 
     def __init__(self, scope: BaseScope):
-        self._factories: dict[type, Factory] = {}
+        self._factories: dict[DependencyKey, Factory] = {}
         self.scope = scope
 
     def add_factory(self, factory: Factory):
-        if is_generic(factory.provides):
-            self._factories[get_origin(factory.provides)] = factory
+        if is_generic(factory.provides.type_hint):
+            origin = get_origin(factory.provides.type_hint)
+            origin_key = DependencyKey(origin, factory.provides.component)
+            self._factories[origin_key] = factory
         else:
             self._factories[factory.provides] = factory
 
-    def get_factory(self, dependency: Any) -> Factory | None:
+    def get_factory(self, dependency: DependencyKey) -> Factory | None:
         try:
             return self._factories[dependency]
         except KeyError:
-            origin = get_origin(dependency)
+            origin = get_origin(dependency.type_hint)
             if not origin:
                 return None
-            factory = self._factories.get(origin)
+            origin_key = DependencyKey(origin, dependency.component)
+            factory = self._factories.get(origin_key)
             if not factory:
                 return None
 
@@ -37,25 +41,30 @@ class Registry:
             return factory
 
     def _specialize_generic(
-            self, factory: Factory, dependency: Any,
+            self, factory: Factory, dependency_key: DependencyKey,
     ) -> Factory:
+        dependency = dependency_key.type_hint
         params_replacement = dict(zip(
-            get_args(factory.provides),
-            get_args(dependency), strict=False,
+            get_args(factory.provides.type_hint),
+            get_args(dependency),
+            strict=False,
         ))
-        new_dependencies = []
+        new_dependencies: list[DependencyKey] = []
         for source_dependency in factory.dependencies:
-            if isinstance(source_dependency, TypeVar):
-                source_dependency = params_replacement[source_dependency]
-            elif get_origin(source_dependency):
-                source_dependency = source_dependency[tuple(
+            hint = source_dependency.type_hint
+            if isinstance(hint, TypeVar):
+                hint = params_replacement[hint]
+            elif get_origin(hint):
+                hint = hint[tuple(
                     params_replacement[param]
-                    for param in get_type_vars(source_dependency)
+                    for param in get_type_vars(hint)
                 )]
-            new_dependencies.append(source_dependency)
+            new_dependencies.append(DependencyKey(
+                hint, source_dependency.component,
+            ))
         return Factory(
             source=factory.source,
-            provides=dependency,
+            provides=dependency_key,
             dependencies=new_dependencies,
             is_to_bind=factory.is_to_bind,
             type_=factory.type,
@@ -67,52 +76,58 @@ class Registry:
 def make_registries(
         *providers: Provider, scopes: type[BaseScope],
 ) -> list[Registry]:
-    dep_scopes: dict[type, BaseScope] = {}
-    alias_sources = {}
+    dep_scopes: dict[DependencyKey, BaseScope] = {}
+    alias_sources: dict[DependencyKey, Any] = {}
     for provider in providers:
+        component = provider.component
         for source in provider.factories:
-            dep_scopes[source.provides] = source.scope
+            provides = source.provides.with_component(component)
+            dep_scopes[provides] = source.scope
         for source in provider.aliases:
-            alias_sources[source.provides] = source.source
+            provides = source.provides.with_component(component)
+            alias_sources[provides] = source.source.with_component(component)
 
     registries = {scope: Registry(scope) for scope in scopes}
-    decorator_depth: dict[type, int] = defaultdict(int)
+    decorator_depth: dict[DependencyKey, int] = defaultdict(int)
 
     for provider in providers:
+        component = provider.component
         for source in provider.factories:
             scope = source.scope
-            registries[scope].add_factory(source)
+            registries[scope].add_factory(source.with_component(component))
         for source in provider.aliases:
-            alias_source = source.source
-            visited_types = [alias_source]
+            alias_source = source.source.with_component(component)
+            visited_keys = [alias_source]
             while alias_source not in dep_scopes:
                 alias_source = alias_sources[alias_source]
-                if alias_source in visited_types:
+                if alias_source in visited_keys:
                     raise InvalidGraphError(
-                        f"Cycle aliases detected {visited_types}",
+                        f"Cycle aliases detected {visited_keys}",
                     )
-                visited_types.append(alias_source)
+                visited_keys.append(alias_source)
             scope = dep_scopes[alias_source]
+            source = source.as_factory(scope, component)
             dep_scopes[source.provides] = scope
-            source = source.as_factory(scope)
             registries[scope].add_factory(source)
         for source in provider.decorators:
-            provides = source.provides
+            provides = source.provides.with_component(component)
             scope = dep_scopes[provides]
             registry = registries[scope]
             undecorated_type = NewType(
-                f"{provides.__name__}@{decorator_depth[provides]}",
+                f"{provides.type_hint.__name__}@{decorator_depth[provides]}",
                 source.provides,
             )
             decorator_depth[provides] += 1
             old_factory = registry.get_factory(provides)
-            old_factory.provides = undecorated_type
+            old_factory.provides = DependencyKey(
+                undecorated_type, old_factory.provides.component,
+            )
             registry.add_factory(old_factory)
             source = source.as_factory(
                 scope=scope,
-                new_dependency=undecorated_type,
+                new_dependency=DependencyKey(undecorated_type, None),
                 cache=old_factory.cache,
+                component=component,
             )
             registries[scope].add_factory(source)
-
     return list(registries.values())


Test patch:
diff --git a/docs/advanced/testing.rst b/docs/advanced/testing.rst
new file mode 100644
index 0000000..5500668
--- /dev/null
+++ b/docs/advanced/testing.rst
@@ -0,0 +1,3 @@
+Testing with dishka
+============================
+
diff --git a/tests/unit/container/test_components.py b/tests/unit/container/test_components.py
new file mode 100644
index 0000000..a0528d0
--- /dev/null
+++ b/tests/unit/container/test_components.py
@@ -0,0 +1,204 @@
+from typing import Annotated
+
+import pytest
+
+from dishka import (
+    Component,
+    FromComponent,
+    Provider,
+    Scope,
+    alias,
+    decorate,
+    make_async_container,
+    make_container,
+    provide,
+)
+from dishka.exceptions import NoFactoryError
+
+
+class MainProvider(Provider):
+    scope = Scope.APP
+
+    def __init__(self, value: int, component: Component | None = None):
+        super().__init__(component=component)
+        self.value = value
+
+    @provide
+    def foo(self, a: Annotated[float, FromComponent("X")]) -> complex:
+        return a * 10
+
+    @provide
+    def bar(self) -> int:
+        return self.value
+
+
+class AliasedProvider(Provider):
+    scope = Scope.APP
+
+    float_alias = alias(source=float, component="X")
+
+    @provide
+    def foo(self, a: float) -> complex:
+        return a * 10
+
+    @provide
+    def bar(self) -> int:
+        return 20
+
+
+class XProvider(Provider):
+    scope = Scope.APP
+    component = "X"
+
+    @provide
+    def foo(self, a: Annotated[int, FromComponent()]) -> float:
+        return a + 1
+
+
+def test_from_component():
+    container = make_container(MainProvider(20), XProvider())
+    assert container.get(complex) == 210
+    assert container.get(float, component="X") == 21
+    with pytest.raises(NoFactoryError):
+        container.get(float)
+
+
+@pytest.mark.asyncio()
+async def test_from_component_async():
+    container = make_async_container(MainProvider(20), XProvider())
+    assert await container.get(complex) == 210
+    assert await container.get(float, component="X") == 21
+    with pytest.raises(NoFactoryError):
+        await container.get(float)
+
+
+def test_change_component():
+    container = make_container(MainProvider(20).to_component("Y"))
+    assert container.get(int, component="Y") == 20
+
+
+def test_set_component():
+    container = make_container(MainProvider(20, component="Y"))
+    assert container.get(int, component="Y") == 20
+
+
+def test_from_component_alias():
+    container = make_container(AliasedProvider(), XProvider())
+    assert container.get(complex) == 210
+    assert container.get(float, component="X") == 21
+    assert container.get(float) == 21
+
+
+class Provider1(Provider):
+    scope = Scope.APP
+    component = "1"
+
+    @provide
+    def foo(self) -> int:
+        return 1
+
+    @provide
+    def bar(self, a: int) -> float:
+        return a
+
+
+class Provider2(Provider):
+    scope = Scope.APP
+    component = "2"
+
+    @provide
+    def foo(self) -> int:
+        return 2
+
+    @provide
+    def bar(self, a: int) -> complex:
+        return a
+
+
+class ProviderSum(Provider):
+    scope = Scope.APP
+
+    @provide
+    def foo(
+            self,
+            f: Annotated[float, FromComponent("1")],
+            c: Annotated[complex, FromComponent("2")],
+    ) -> int:
+        return int(10 * c + f)
+
+
+def test_isolated_component():
+    container = make_container(Provider1(), Provider2(), ProviderSum())
+    assert container.get(int) == 21
+
+
+class ProviderInc(Provider):
+    scope = Scope.APP
+    component = "X"
+
+    def __init__(self):
+        super().__init__()
+        self.value = 1
+
+    @provide
+    def foo(self) -> int:
+        self.value += 1
+        return self.value
+
+
+def test_cache():
+    provider = Provider(scope=Scope.APP)
+    provider.provide(lambda: 1, provides=int)
+    provider_inc = ProviderInc()
+    container = make_container(provider, provider_inc)
+    assert container.get(int, component="X") == 2
+    assert container.get(int, component="X") == 2
+    assert container.get(int) == 1
+    container = make_container(provider, provider_inc)
+    assert container.get(int, component="X") == 3
+    assert container.get(int, component="X") == 3
+
+    container = make_container(provider_inc.to_component("Y"), provider_inc)
+    assert container.get(int, component="X") == 4
+    assert container.get(int, component="X") == 4
+    assert container.get(int, component="Y") == 5
+    assert container.get(int, component="X") == 4
+
+
+@pytest.mark.asyncio
+async def test_cache_async():
+    provider = Provider(scope=Scope.APP)
+    provider.provide(lambda: 1, provides=int)
+    provider_inc = ProviderInc()
+    container = make_async_container(provider, provider_inc)
+    assert await container.get(int, component="X") == 2
+    assert await container.get(int, component="X") == 2
+    assert await container.get(int) == 1
+    container = make_async_container(provider, provider_inc)
+    assert await container.get(int, component="X") == 3
+    assert await container.get(int, component="X") == 3
+
+    container = make_async_container(provider_inc.to_component("Y"),
+                                     provider_inc)
+    assert await container.get(int, component="X") == 4
+    assert await container.get(int, component="X") == 4
+    assert await container.get(int, component="Y") == 5
+    assert await container.get(int, component="X") == 4
+
+
+class ProviderDecorated(Provider):
+    @decorate
+    def foo(self, a: int) -> int:
+        return a + 1
+
+
+def test_decorator():
+    dec_provider = ProviderDecorated(scope=Scope.APP, component="X")
+    x_provider = Provider(scope=Scope.APP, component="X")
+    x_provider.provide(lambda: 1, provides=int)
+    provider = Provider(scope=Scope.APP)
+    provider.provide(lambda: 100, provides=int)
+
+    container = make_container(provider, x_provider, dec_provider)
+    assert container.get(int, component="X") == 2
+    assert container.get(int) == 100
diff --git a/tests/unit/container/test_exceptions.py b/tests/unit/container/test_exceptions.py
index eae6ebe..cea0358 100644
--- a/tests/unit/container/test_exceptions.py
+++ b/tests/unit/container/test_exceptions.py
@@ -5,6 +5,8 @@ from unittest.mock import Mock
 import pytest
 
 from dishka import (
+    DEFAULT_COMPONENT,
+    DependencyKey,
     Provider,
     Scope,
     make_async_container,
@@ -105,8 +107,12 @@ def test_no_factory_sync():
     container = make_container(InvalidScopeProvider())
     with pytest.raises(NoFactoryError) as e:
         container.get(complex)
-    assert e.value.requested == object
-    assert e.value.path == [complex, float, int]
+    assert e.value.requested == DependencyKey(object, DEFAULT_COMPONENT)
+    assert e.value.path == [
+        DependencyKey(complex, DEFAULT_COMPONENT),
+        DependencyKey(float, DEFAULT_COMPONENT),
+        DependencyKey(int, DEFAULT_COMPONENT),
+    ]
 
 
 @pytest.mark.asyncio
@@ -114,8 +120,12 @@ async def test_no_factory_async():
     container = make_async_container(InvalidScopeProvider())
     with pytest.raises(NoFactoryError) as e:
         await container.get(complex)
-    assert e.value.requested == object
-    assert e.value.path == [complex, float, int]
+    assert e.value.requested == DependencyKey(object, DEFAULT_COMPONENT)
+    assert e.value.path == [
+        DependencyKey(complex, DEFAULT_COMPONENT),
+        DependencyKey(float, DEFAULT_COMPONENT),
+        DependencyKey(int, DEFAULT_COMPONENT),
+    ]
 
 
 class AsyncProvider(Provider):
diff --git a/tests/unit/sample_providers.py b/tests/unit/sample_providers.py
index 47d6bad..5f05e81 100644
--- a/tests/unit/sample_providers.py
+++ b/tests/unit/sample_providers.py
@@ -1,7 +1,7 @@
 from collections.abc import AsyncGenerator, AsyncIterable, Generator, Iterable
 from typing import Any
 
-from dishka import Scope
+from dishka import DependencyKey, Scope
 from dishka.dependency_source import Factory, FactoryType
 
 
@@ -45,7 +45,7 @@ async def async_gen_a(self, dep: int) -> AsyncGenerator[ClassA, None]:
 
 A_VALUE = ClassA(42)
 value_factory = Factory(
-    provides=ClassA,
+    provides=DependencyKey(ClassA, None),
     source=A_VALUE,
     dependencies=[],
     type_=FactoryType.VALUE,
diff --git a/tests/unit/test_provider.py b/tests/unit/test_provider.py
index d92c250..cd76695 100644
--- a/tests/unit/test_provider.py
+++ b/tests/unit/test_provider.py
@@ -4,6 +4,10 @@ import pytest
 
 from dishka import Provider, Scope, alias, provide
 from dishka.dependency_source import FactoryType
+from dishka.entities.key import (
+    hint_to_dependency_key,
+    hints_to_dependency_keys,
+)
 from .sample_providers import (
     ClassA,
     async_func_a,
@@ -41,8 +45,8 @@ def test_provider_init():
 )
 def test_parse_factory(source, provider_type, is_to_bound):
     factory = provide(source, scope=Scope.REQUEST)
-    assert factory.provides == ClassA
-    assert factory.dependencies == [Any, int]
+    assert factory.provides == hint_to_dependency_key(ClassA)
+    assert factory.dependencies == hints_to_dependency_keys([Any, int])
     assert factory.is_to_bind == is_to_bound
     assert factory.scope == Scope.REQUEST
     assert factory.source == source
@@ -57,8 +61,8 @@ def test_parse_factory(source, provider_type, is_to_bound):
 )
 def test_parse_factory_cls(source, provider_type, is_to_bound):
     factory = provide(source, scope=Scope.REQUEST)
-    assert factory.provides == ClassA
-    assert factory.dependencies == [int]
+    assert factory.provides == hint_to_dependency_key(ClassA)
+    assert factory.dependencies == hints_to_dependency_keys([int])
     assert factory.is_to_bind == is_to_bound
     assert factory.scope == Scope.REQUEST
     assert factory.source == source
@@ -139,27 +143,27 @@ def test_callable():
         foo = provide(MyCallable())
 
     provider = MyProvider(scope=Scope.REQUEST)
-    assert provider.foo.provides == str
-    assert provider.foo.dependencies == [int]
+    assert provider.foo.provides == hint_to_dependency_key(str)
+    assert provider.foo.dependencies == hints_to_dependency_keys([int])
 
 
 def test_provide_as_method():
     provider = Provider(scope=Scope.REQUEST)
     foo = provider.provide(MyCallable())
-    assert foo.provides == str
-    assert foo.dependencies == [int]
+    assert foo.provides == hint_to_dependency_key(str)
+    assert foo.dependencies == hints_to_dependency_keys([int])
 
     foo = provider.provide(sync_func_a)
-    assert foo.provides == ClassA
-    assert foo.dependencies == [Any, int]
+    assert foo.provides == hint_to_dependency_key(ClassA)
+    assert foo.dependencies == hints_to_dependency_keys([Any, int])
 
     foo = provider.alias(source=int, provides=str)
-    assert foo.provides == str
-    assert foo.source == int
+    assert foo.provides == hint_to_dependency_key(str)
+    assert foo.source == hint_to_dependency_key(int)
 
     foo = provider.decorate(sync_func_a)
-    assert foo.provides == ClassA
-    assert foo.factory.dependencies == [Any, int]
+    assert foo.provides == hint_to_dependency_key(ClassA)
+    assert foo.factory.dependencies == hints_to_dependency_keys([Any, int])
 
 
 class OtherClass:
@@ -170,5 +174,5 @@ class OtherClass:
 def test_provide_external_method():
     provider = Provider(scope=Scope.REQUEST)
     foo = provider.provide(OtherClass().method)
-    assert foo.provides == str
+    assert foo.provides == hint_to_dependency_key(str)
     assert foo.dependencies == []