diff --git a/plugins/module_utils/common.py b/plugins/module_utils/common.py
new file mode 100644
index 0000000..495ea6d
--- /dev/null
+++ b/plugins/module_utils/common.py
@@ -0,0 +1,67 @@
+# Copyright: (c) 2024, Luca Bilke <luca@bil.ke>
+# MIT License (see LICENSE)
+
+from __future__ import annotations
+
+from typing import Any
+import yaml
+from pydantic import BaseModel, ConfigDict
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+class Result(BaseModel):
+    model_config = ConfigDict(frozen=True)
+
+    changed: bool = False
+    diff: dict[str, Any] = {}
+
+
+class Settings(BaseModel):
+    model_config = ConfigDict(frozen=True)
+
+    projects_dir: str = "/usr/local/share/ez_compose/"
+
+
+class State(BaseModel):
+    model_config = ConfigDict(frozen=True)
+
+    module: AnsibleModule
+    result: Result
+    settings: Settings
+    compose_filename: str
+    before: dict[str, Any] = {}
+    after: dict[str, Any] = {}
+
+
+def new_state(module: AnsibleModule) -> State:
+    settings = Settings(**module.params["settings"])
+
+    return State(
+        module=module,
+        result=Result(),
+        settings=settings,
+        compose_filename=(
+            f"{settings.projects_dir}/"
+            + f"{module.params['project_name']}/"
+            + f"{module.params['name']}.yml"
+        ),
+    )
+
+
+def get_compose(state: State) -> State:
+    file = state.compose_filename
+
+    with open(file, "r") as stream:
+        compose = yaml.safe_load(stream)
+
+    return state.model_copy(update={"before": compose})
+
+
+def write_compose(state: State) -> State:
+    file = state.compose_filename
+
+    with open(file, mode="w") as stream:
+        yaml.dump(state.after, stream)
+
+    return state
diff --git a/plugins/modules/redis.py b/plugins/modules/redis.py
new file mode 100644
index 0000000..fe05bc1
--- /dev/null
+++ b/plugins/modules/redis.py
@@ -0,0 +1,77 @@
+# Copyright: (c) 2024, Luca Bilke <luca@bil.ke>
+# MIT License (see LICENSE)
+
+from __future__ import annotations
+
+import copy
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.snailed.ez_compose.plugins.module_utils.common import (
+    State,
+    get_compose,
+    new_state,
+    write_compose,
+)
+
+
+def generate(state: State) -> State:
+    params = state.module.params
+    compose = copy.deepcopy(state.before)
+
+    services = compose.get("services", {})
+
+    container = services.get(params["name"], {})
+    container.update(
+        {
+            "image": params["image"],
+        }
+    )
+
+    services.update({params["name"]: container})
+
+    return state.model_copy(update={"after": compose})
+
+
+def main():
+    module = AnsibleModule(
+        argument_spec={
+            "state": {
+                "type": "str",
+                "default": "present",
+                "choices": ["present", "absent"],
+            },
+            "name": {
+                "type": "str",
+                "required": True,
+            },
+            "project_name": {
+                "type": "str",
+                "required": True,
+            },
+            "image": {
+                "type": "str",
+            },
+            "settings": {
+                "type": "dict",
+                "required": True,
+            },
+        },
+        supports_check_mode=True,
+    )
+
+    state = new_state(module)
+
+    get_compose(state)
+
+    generate(state)
+
+    if module.check_mode:
+        module.exit_json(**state.result)  # type: ignore[reportUnkownMemberType]
+
+    write_compose(state)
+
+    module.exit_json(**state.result)  # type: ignore[reportUnkownMemberType]
+
+
+if __name__ == "__main__":
+    main()
diff --git a/plugins/modules/template b/plugins/modules/template
deleted file mode 100644
index 20c2348..0000000
--- a/plugins/modules/template
+++ /dev/null
@@ -1,24 +0,0 @@
-#!/usr/bin/python
-
-# Copyright: (c) 2024, Luca Bilke <luca@bil.ke>
-# MIT License (see LICENSE)
-
-from __future__ import annotations
-from ansible.module_utils.basic import AnsibleModule
-
-
-def main():
-    global module
-
-    module = AnsibleModule(
-        argument_spec={
-            "state": {"type": "str", "default": "present", "choices": ["present", "absent"]},
-            "name": {"type": "str"},
-            "image": {"type": "str"},
-        },
-        supports_check_mode=True,
-    )
-
-
-if __name__ == "__main__":
-    main()
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..d0faa40
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,2 @@
+ansible-core==2.17.*
+pydantic==2.9.*