Repository: cobbler/cobbler (language: python)

Issue:
Delay reading collections during startup
### Is your feature request related to a problem?

As a Cobbler administrator
I want to have a quicker startup of Cobbler
so that I can restart the daemon with less downtime.

### Provide a detailed description of the proposed feature

Currently, in bigger environments, the startup of Cobbler is too slow. Since we can optimize the startup procedure only so much it is reasonable to have a delayed initialization of the database that temporarily causes Cobbler to not have its full dataset available.

### Alternatives you've considered

None

### Additional information

Splitout of #3301 



Discussion / hints:
The current implementation in #3363 requires us to call an XML-RPC method during daemon startup. This feels wrong.

The alternative that @tpw56j proposed is creating two new triggers that are available to be executed during startup and shutdown.

We also have a problem at the moment (partly linked to #3148) which is that the XML-RPC API is not just an XML-RPC API but also provides functionallity that is exclusively available there. The thing that is of concern for this issue is that we cannot start background tasks with the current structure of the API. This prevents us from moving the task of lazy-loading from `remote.py` to `api.py`. Linked to this is the problem that we skip the built-in authentication because we don't have a token during daemon startup yet. However, if we load the authentication module is loaded we are at a point where we could server request thus signalling to API consumers that we are ready to server requests already (which is not the case because we didn't load the data yet).
The background task of loading all objects in the implementation https://github.com/cobbler/cobbler/pull/3363 plays an auxiliary role and can be executed at any time later after the creation of the `CobblerAPI` object. You can even refuse to run this task altogether. In this case, there will be a delay when the object is first accessed.
@tpw56j This is your initial work. I would give you the final word.

My opinion is that the penalty for not loading all objects during idle times is very low since probably people just spam a `cobbler sync` which would force most items to be present anyway.
It also seems to me that it is possible to opt out of forcing objects to be loaded during idle time, at least until such time as there is a satisfactory way to run such a task. Those who need lazy loading have a huge number of objects, but most likely the active set of objects is much smaller. And in the case of `cobbler sync`, the additional time for reading objects will be relatively small.
It seems to me that the last word would be good to give to those who use a huge set of objects. In my practice, I didn’t have such a set that I needed delayed loading, I was interested in the task itself.
Alternative: if for some reason it is necessary to have all objects in memory, then we can assign this responsibility to the user, giving the opportunity to start this background job on his own with a command (or via API) something like `cobbler load`.
I do not really like the idea, I bring it only for the sake of completeness of alternatives.
After giving this some thought I have come to the following conclusion: We will not implement the background task in the linked PR. The issue is considered done after the PR is merged.

The reasoning behind that is the following: We have the issues #1283 & #3148. Both issues together state that we should move much of the logic in `remote.py` (thus including starting and managing background tasks) to the `api.py` module. Doing this in the linked PR looks out-of-scope. Since we cannot generate a representative number of users with big systems sadly, there is also no way to know the impact of my decision at this point in time. The worst-case scenario for this is acceptable because it is a one-time penalty which is not a problem for a long-running process like `cobblerd`.

Fix patch:
diff --git a/cobbler/actions/replicate.py b/cobbler/actions/replicate.py
index a2e7fb572d..c2b958942c 100644
--- a/cobbler/actions/replicate.py
+++ b/cobbler/actions/replicate.py
@@ -136,8 +136,7 @@ def add_objects_not_on_local(self, obj_type: str):
 
             if not rdata["uid"] in local_objects:
                 creator = getattr(self.api, f"new_{obj_type}")
-                newobj = creator()
-                newobj.from_dict(utils.revert_strip_none(rdata))
+                newobj = creator(utils.revert_strip_none(rdata))
                 try:
                     self.logger.info("adding %s %s", obj_type, rdata["name"])
                     if not self.api.add_item(obj_type, newobj):
@@ -170,8 +169,7 @@ def replace_objects_newer_on_remote(self, obj_type: str):
                         self.logger.info("removing %s %s", obj_type, ldata["name"])
                         self.api.remove_item(obj_type, ldata["name"], recursive=True)
                     creator = getattr(self.api, f"new_{obj_type}")
-                    newobj = creator()
-                    newobj.from_dict(utils.revert_strip_none(rdata))
+                    newobj = creator(utils.revert_strip_none(rdata))
                     try:
                         self.logger.info("updating %s %s", obj_type, rdata["name"])
                         if not self.api.add_item(obj_type, newobj):
diff --git a/cobbler/api.py b/cobbler/api.py
index 1795fe7b27..9541172dba 100644
--- a/cobbler/api.py
+++ b/cobbler/api.py
@@ -14,7 +14,7 @@
 import threading
 from configparser import ConfigParser
 from pathlib import Path
-from typing import TYPE_CHECKING, Dict, List, Optional, Union
+from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
 
 from schema import SchemaError
 
@@ -61,6 +61,7 @@
 
 if TYPE_CHECKING:
     from cobbler.settings import Settings
+    from cobbler.items import item
 
 
 # notes on locking:
@@ -76,7 +77,7 @@ class CobblerAPI:
     Cli apps and daemons should import api.py, and no other Cobbler code.
     """
 
-    __shared_state = {}
+    __shared_state: Dict[str, Any] = {}
     __has_loaded = False
 
     def __init__(
@@ -957,7 +958,7 @@ def rename_menu(self, ref, newname: str):
 
     # ==========================================================================
 
-    def new_item(self, what: str = "", is_subobject: bool = False):
+    def new_item(self, what: str = "", is_subobject: bool = False, **kwargs: Any):
         """
         Creates a new (unconfigured) object. The object is not persisted.
 
@@ -968,22 +969,22 @@ def new_item(self, what: str = "", is_subobject: bool = False):
         """
         try:
             enums.ItemTypes(what)  # verify that <what> is an ItemTypes member
-            return getattr(self, f"new_{what}")(is_subobject=is_subobject)
+            return getattr(self, f"new_{what}")(is_subobject, **kwargs)
         except (ValueError, AttributeError) as error:
             raise Exception(f"internal error, collection name is {what}") from error
 
-    def new_distro(self, is_subobject: bool = False):
+    def new_distro(self, is_subobject: bool = False, **kwargs: Any) -> distro.Distro:
         """
         Returns a new empty distro object. This distro is not automatically persisted. Persistance is achieved via
         ``save()``.
 
-        :param is_subobject: If the object created is a subobject or not.
+        :param is_subobject: If the object is a subobject of an already existing object or not.
         :return: An empty Distro object.
         """
-        self.log("new_distro", [is_subobject])
-        return distro.Distro(self, is_subobject=is_subobject)
+        self.log("new_distro", kwargs)
+        return distro.Distro(self, is_subobject, **kwargs)
 
-    def new_profile(self, is_subobject: bool = False):
+    def new_profile(self, is_subobject: bool = False, **kwargs: Any) -> profile.Profile:
         """
         Returns a new empty profile object. This profile is not automatically persisted. Persistence is achieved via
         ``save()``.
@@ -991,10 +992,10 @@ def new_profile(self, is_subobject: bool = False):
         :param is_subobject: If the object created is a subobject or not.
         :return: An empty Profile object.
         """
-        self.log("new_profile", [is_subobject])
-        return profile.Profile(self, is_subobject=is_subobject)
+        self.log("new_profile", kwargs)
+        return profile.Profile(self, is_subobject, kwargs)
 
-    def new_system(self, is_subobject: bool = False):
+    def new_system(self, is_subobject: bool = False, **kwargs: Any):
         """
         Returns a new empty system object. This system is not automatically persisted. Persistence is achieved via
         ``save()``.
@@ -1002,10 +1003,10 @@ def new_system(self, is_subobject: bool = False):
         :param is_subobject: If the object created is a subobject or not.
         :return: An empty System object.
         """
-        self.log("new_system", [is_subobject])
-        return system.System(self, is_subobject=is_subobject)
+        self.log("new_system", kwargs)
+        return system.System(self, is_subobject, kwargs)
 
-    def new_repo(self, is_subobject: bool = False):
+    def new_repo(self, is_subobject: bool = False, **kwargs: Any):
         """
         Returns a new empty repo object. This repository is not automatically persisted. Persistence is achieved via
         ``save()``.
@@ -1013,10 +1014,10 @@ def new_repo(self, is_subobject: bool = False):
         :param is_subobject: If the object created is a subobject or not.
         :return: An empty repo object.
         """
-        self.log("new_repo", [is_subobject])
-        return repo.Repo(self, is_subobject=is_subobject)
+        self.log("new_repo", kwargs)
+        return repo.Repo(self, is_subobject, kwargs)
 
-    def new_image(self, is_subobject: bool = False):
+    def new_image(self, is_subobject: bool = False, **kwargs: Any):
         """
         Returns a new empty image object. This image is not automatically persisted. Persistence is achieved via
         ``save()``.
@@ -1024,10 +1025,10 @@ def new_image(self, is_subobject: bool = False):
         :param is_subobject: If the object created is a subobject or not.
         :return: An empty image object.
         """
-        self.log("new_image", [is_subobject])
-        return image.Image(self, is_subobject=is_subobject)
+        self.log("new_image", kwargs)
+        return image.Image(self, is_subobject, kwargs)
 
-    def new_mgmtclass(self, is_subobject: bool = False):
+    def new_mgmtclass(self, is_subobject: bool = False, **kwargs: Any):
         """
         Returns a new empty mgmtclass object. This mgmtclass is not automatically persisted. Persistence is achieved via
         ``save()``.
@@ -1035,10 +1036,10 @@ def new_mgmtclass(self, is_subobject: bool = False):
         :param is_subobject: If the object created is a subobject or not.
         :return: An empty mgmtclass object.
         """
-        self.log("new_mgmtclass", [is_subobject])
-        return mgmtclass.Mgmtclass(self, is_subobject=is_subobject)
+        self.log("new_mgmtclass", kwargs)
+        return mgmtclass.Mgmtclass(self, is_subobject, kwargs)
 
-    def new_package(self, is_subobject: bool = False):
+    def new_package(self, is_subobject: bool = False, **kwargs: Any):
         """
         Returns a new empty package object. This package is not automatically persisted. Persistence is achieved via
         ``save()``.
@@ -1046,10 +1047,10 @@ def new_package(self, is_subobject: bool = False):
         :param is_subobject: If the object created is a subobject or not.
         :return: An empty Package object.
         """
-        self.log("new_package", [is_subobject])
-        return package.Package(self, is_subobject=is_subobject)
+        self.log("new_package", kwargs)
+        return package.Package(self, is_subobject, kwargs)
 
-    def new_file(self, is_subobject: bool = False):
+    def new_file(self, is_subobject: bool = False, **kwargs: Any):
         """
         Returns a new empty file object. This file is not automatically persisted. Persistence is achieved via
         ``save()``.
@@ -1057,10 +1058,10 @@ def new_file(self, is_subobject: bool = False):
         :param is_subobject: If the object created is a subobject or not.
         :return: An empty File object.
         """
-        self.log("new_file", [is_subobject])
-        return file.File(self, is_subobject=is_subobject)
+        self.log("new_file", kwargs)
+        return file.File(self, is_subobject, kwargs)
 
-    def new_menu(self, is_subobject: bool = False):
+    def new_menu(self, is_subobject: bool = False, **kwargs: Any):
         """
         Returns a new empty menu object. This file is not automatically persisted. Persistence is achieved via
         ``save()``.
@@ -1068,8 +1069,8 @@ def new_menu(self, is_subobject: bool = False):
         :param is_subobject: If the object created is a subobject or not.
         :return: An empty File object.
         """
-        self.log("new_menu", [is_subobject])
-        return menu.Menu(self, is_subobject=is_subobject)
+        self.log("new_menu", kwargs)
+        return menu.Menu(self, is_subobject, kwargs)
 
     # ==========================================================================
 
@@ -2029,6 +2030,13 @@ def deserialize(self):
         """
         return self._collection_mgr.deserialize()
 
+    def deserialize_item(self, obj: "item.Item") -> Dict[str, Any]:
+        """
+        Load cobbler item from disk.
+        Cobbler internal use only.
+        """
+        return self._collection_mgr.deserialize_one_item(obj)
+
     # ==========================================================================
 
     def get_module_by_name(self, module_name: str):
diff --git a/cobbler/cobbler_collections/collection.py b/cobbler/cobbler_collections/collection.py
index df3635c255..8f1b1ef5d0 100644
--- a/cobbler/cobbler_collections/collection.py
+++ b/cobbler/cobbler_collections/collection.py
@@ -9,7 +9,6 @@
 import logging
 import time
 import os
-import uuid
 from threading import Lock
 from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
 
@@ -108,6 +107,14 @@ def get(self, name: str) -> Optional[item_base.Item]:
         """
         return self.listing.get(name, None)
 
+    def get_names(self) -> List[str]:
+        """
+        Return list of names in the collection.
+
+        :return: list of names in the collection.
+        """
+        return list(self.listing)
+
     def find(
         self,
         name: str = "",
@@ -146,8 +153,15 @@ def find(
             except Exception:
                 return self.listing.get(kargs["name"], None)
 
+        if self.api.settings().lazy_start:
+            # Forced deserialization of the entire collection to prevent deadlock in the search loop
+            for obj_name in self.get_names():
+                obj = self.get(obj_name)
+                if obj is not None and not obj.inmemory:
+                    obj.deserialize()
+
         with self.lock:
-            for (_, obj) in list(self.listing.items()):
+            for obj in self:
                 if obj.find_match(kargs, no_errors=no_errors):
                     matches.append(obj)
 
@@ -218,9 +232,11 @@ def from_list(self, _list: List[Dict[str, Any]]):
                 item = self.factory_produce(self.api, item_dict)
                 self.add(item)
             except Exception as exc:
+                print(item_dict)
                 self.logger.error(
-                    "Error while loading a collection: %s. Skipping this collection!",
+                    "Error while loading a collection: %s. Skipping collection %s!",
                     exc,
+                    self.collection_type(),
                 )
 
     def copy(self, ref: item_base.Item, newname: str):
@@ -230,20 +246,19 @@ def copy(self, ref: item_base.Item, newname: str):
         :param ref: The reference to the object which should be copied.
         :param newname: The new name for the copied object.
         """
-        ref = ref.make_clone()
-        ref.uid = uuid.uuid4().hex
-        ref.ctime = time.time()
-        ref.name = newname
-        if ref.COLLECTION_TYPE == "system":
+        copied_item = ref.make_clone()
+        copied_item.ctime = time.time()
+        copied_item.name = newname
+        if copied_item.COLLECTION_TYPE == "system":
             # this should only happen for systems
-            for interface in ref.interfaces:
+            for interface in copied_item.interfaces:
                 # clear all these out to avoid DHCP/DNS conflicts
-                ref.interfaces[interface].dns_name = ""
-                ref.interfaces[interface].mac_address = ""
-                ref.interfaces[interface].ip_address = ""
+                copied_item.interfaces[interface].dns_name = ""
+                copied_item.interfaces[interface].mac_address = ""
+                copied_item.interfaces[interface].ip_address = ""
 
         self.add(
-            ref,
+            copied_item,
             save=True,
             with_copy=True,
             with_triggers=True,
@@ -368,9 +383,6 @@ def add(
 
         ref.check_if_valid()
 
-        if ref.uid == "":
-            ref.uid = uuid.uuid4().hex
-
         if save:
             now = float(time.time())
             if ref.ctime == 0.0:
@@ -413,8 +425,6 @@ def add(
         if save:
             # Save just this item if possible, if not, save the whole collection
             self.collection_mgr.serialize_one_item(ref)
-            if ref.parent:
-                self.collection_mgr.serialize_one_item(ref.parent)
 
             if with_sync:
                 if isinstance(ref, system.System):
diff --git a/cobbler/cobbler_collections/distros.py b/cobbler/cobbler_collections/distros.py
index 9e7aaab553..83031ea147 100644
--- a/cobbler/cobbler_collections/distros.py
+++ b/cobbler/cobbler_collections/distros.py
@@ -8,6 +8,7 @@
 
 import os.path
 import glob
+from typing import TYPE_CHECKING, Any, Dict
 
 from cobbler.cobbler_collections import collection
 from cobbler.items import distro
@@ -15,6 +16,9 @@
 from cobbler.cexceptions import CX
 from cobbler.utils import filesystem_helpers
 
+if TYPE_CHECKING:
+    from cobbler.api import CobblerAPI
+
 
 class Distros(collection.Collection):
     """
@@ -29,17 +33,19 @@ def collection_type() -> str:
     def collection_types() -> str:
         return "distros"
 
-    def factory_produce(self, api, item_dict):
+    def factory_produce(self, api: "CobblerAPI", seed_data: Dict[str, Any]):
         """
-        Return a Distro forged from item_dict
+        Return a Distro forged from seed_data
+
+        :param api: Parameter is skipped.
+        :param seed_data: Data to seed the object with.
+        :returns: The created object.
         """
-        new_distro = distro.Distro(api)
-        new_distro.from_dict(item_dict)
-        return new_distro
+        return distro.Distro(self.api, **seed_data)
 
     def remove(
         self,
-        name,
+        name: str,
         with_delete: bool = True,
         with_sync: bool = True,
         with_triggers: bool = True,
@@ -62,7 +68,6 @@ def remove(
                 if profile.distro and profile.distro.name == name:
                     raise CX(f"removal would orphan profile: {profile.name}")
 
-        kernel = obj.kernel
         if recursive:
             kids = self.api.find_items("profile", {"distro": obj.name})
             for k in kids:
@@ -100,8 +105,9 @@ def remove(
         settings = self.api.settings()
         possible_storage = glob.glob(settings.webdir + "/distro_mirror/*")
         path = None
+        kernel = obj.kernel
         for storage in possible_storage:
-            if os.path.dirname(obj.kernel).find(storage) != -1:
+            if os.path.dirname(kernel).find(storage) != -1:
                 path = storage
                 continue
 
diff --git a/cobbler/cobbler_collections/files.py b/cobbler/cobbler_collections/files.py
index 604a0f9f6c..51dcc5e1e4 100644
--- a/cobbler/cobbler_collections/files.py
+++ b/cobbler/cobbler_collections/files.py
@@ -5,11 +5,15 @@
 # SPDX-License-Identifier: GPL-2.0-or-later
 # SPDX-FileCopyrightText: Copyright 2010, Kelsey Hightower <kelsey.hightower@gmail.com>
 
+from typing import TYPE_CHECKING, Any, Dict
 from cobbler.cobbler_collections import collection
-from cobbler.items import file as file
+from cobbler.items import file
 from cobbler import utils
 from cobbler.cexceptions import CX
 
+if TYPE_CHECKING:
+    from cobbler.api import CobblerAPI
+
 
 class Files(collection.Collection):
     """
@@ -24,17 +28,19 @@ def collection_type() -> str:
     def collection_types() -> str:
         return "files"
 
-    def factory_produce(self, api, item_dict):
+    def factory_produce(self, api: "CobblerAPI", seed_data: Dict[str, Any]):
         """
-        Return a File forged from item_dict
+        Return a File forged from seed_data
+
+        :param api: Parameter is skipped.
+        :param seed_data: Data to seed the object with.
+        :returns: The created object.
         """
-        new_file = file.File(api)
-        new_file.from_dict(item_dict)
-        return new_file
+        return file.File(api, **seed_data)
 
     def remove(
         self,
-        name,
+        name: str,
         with_delete: bool = True,
         with_sync: bool = True,
         with_triggers: bool = True,
diff --git a/cobbler/cobbler_collections/images.py b/cobbler/cobbler_collections/images.py
index 52b757bd95..9003496512 100644
--- a/cobbler/cobbler_collections/images.py
+++ b/cobbler/cobbler_collections/images.py
@@ -6,11 +6,15 @@
 # SPDX-FileCopyrightText: Copyright 2006-2009, Red Hat, Inc and Others
 # SPDX-FileCopyrightText: Michael DeHaan <michael.dehaan AT gmail>
 
+from typing import TYPE_CHECKING, Any, Dict
 from cobbler.cobbler_collections import collection
-from cobbler.items import image as image
+from cobbler.items import image
 from cobbler import utils
 from cobbler.cexceptions import CX
 
+if TYPE_CHECKING:
+    from cobbler.api import CobblerAPI
+
 
 class Images(collection.Collection):
     """
@@ -26,17 +30,19 @@ def collection_type() -> str:
     def collection_types() -> str:
         return "images"
 
-    def factory_produce(self, api, item_dict):
+    def factory_produce(self, api: "CobblerAPI", seed_data: Dict[str, Any]):
         """
-        Return a Distro forged from item_dict
+        Return a Distro forged from seed_data
+
+        :param api: Parameter is skipped.
+        :param seed_data: Data to seed the object with.
+        :returns: The created object.
         """
-        new_image = image.Image(api)
-        new_image.from_dict(item_dict)
-        return new_image
+        return image.Image(self.api, **seed_data)
 
     def remove(
         self,
-        name,
+        name: str,
         with_delete: bool = True,
         with_sync: bool = True,
         with_triggers: bool = True,
diff --git a/cobbler/cobbler_collections/manager.py b/cobbler/cobbler_collections/manager.py
index b45bba51bd..c9e007f539 100644
--- a/cobbler/cobbler_collections/manager.py
+++ b/cobbler/cobbler_collections/manager.py
@@ -13,6 +13,7 @@
 from cobbler import serializer
 from cobbler import validate
 from cobbler.settings import Settings
+from cobbler.items.item import Item
 from cobbler.cobbler_collections.distros import Distros
 from cobbler.cobbler_collections.files import Files
 from cobbler.cobbler_collections.images import Images
@@ -185,8 +186,6 @@ def deserialize(self):
 
         :raises CX: if there is an error in deserialization
         """
-        old_cache_enabled = self.api.settings().cache_enabled
-        self.api.settings().cache_enabled = False
         for collection in (
             self._menus,
             self._distros,
@@ -205,7 +204,15 @@ def deserialize(self):
                     f"serializer: error loading collection {collection.collection_type()}: {error}."
                     f"Check your settings!"
                 ) from error
-        self.api.settings().cache_enabled = old_cache_enabled
+
+    def deserialize_one_item(self, obj: Item) -> dict:
+        """
+        Load a collection item from disk
+
+        :param obj: collection item
+        """
+        collection_type = self.get_items(obj.COLLECTION_TYPE).collection_types()
+        return self.__serializer.deserialize_item(collection_type, obj.name)
 
     def get_items(
         self, collection_type: str
diff --git a/cobbler/cobbler_collections/menus.py b/cobbler/cobbler_collections/menus.py
index 20ce7b72cf..d84f30ea1c 100644
--- a/cobbler/cobbler_collections/menus.py
+++ b/cobbler/cobbler_collections/menus.py
@@ -5,11 +5,15 @@
 # SPDX-License-Identifier: GPL-2.0-or-later
 # SPDX-FileCopyrightText: Copyright 2021 Yuriy Chelpanov <yuriy.chelpanov@gmail.com>
 
+from typing import TYPE_CHECKING, Any, Dict
 from cobbler.items import menu
 from cobbler.cobbler_collections import collection
 from cobbler import utils
 from cobbler.cexceptions import CX
 
+if TYPE_CHECKING:
+    from cobbler.api import CobblerAPI
+
 
 class Menus(collection.Collection):
     """
@@ -24,17 +28,15 @@ def collection_type() -> str:
     def collection_types() -> str:
         return "menus"
 
-    def factory_produce(self, api, item_dict):
+    def factory_produce(self, api: "CobblerAPI", seed_data: Dict[str, Any]):
         """
-        Return a Menu forged from item_dict
+        Return a Menu forged from seed_data
 
-        :param api: The cobblerd API.
-        :param item_dict: The seed data.
-        :return: A new menu instance.
+        :param api: Parameter is skipped.
+        :param seed_data: Data to seed the object with.
+        :returns: The created object.
         """
-        new_menu = menu.Menu(api)
-        new_menu.from_dict(item_dict)
-        return new_menu
+        return menu.Menu(self.api, **seed_data)
 
     def remove(
         self,
diff --git a/cobbler/cobbler_collections/mgmtclasses.py b/cobbler/cobbler_collections/mgmtclasses.py
index bbaa3835ec..8f41107bde 100644
--- a/cobbler/cobbler_collections/mgmtclasses.py
+++ b/cobbler/cobbler_collections/mgmtclasses.py
@@ -5,11 +5,15 @@
 # SPDX-License-Identifier: GPL-2.0-or-later
 # SPDX-FileCopyrightText: Copyright 2010, Kelsey Hightower <kelsey.hightower@gmail.com>
 
+from typing import TYPE_CHECKING, Any, Dict
 from cobbler.cobbler_collections import collection
-from cobbler.items import mgmtclass as mgmtclass
+from cobbler.items import mgmtclass
 from cobbler import utils
 from cobbler.cexceptions import CX
 
+if TYPE_CHECKING:
+    from cobbler.api import CobblerAPI
+
 
 class Mgmtclasses(collection.Collection):
     """
@@ -24,21 +28,19 @@ def collection_type() -> str:
     def collection_types() -> str:
         return "mgmtclasses"
 
-    def factory_produce(self, api, item_dict):
+    def factory_produce(self, api: "CobblerAPI", seed_data: Dict[str, Any]):
         """
-        Return a mgmtclass forged from item_dict
+        Return a mgmtclass forged from seed_data
 
-        :param api: TODO
-        :param item_dict: TODO
-        :returns: TODO
+        :param api: Parameter is skipped.
+        :param seed_data: Data to seed the object with.
+        :returns: The created object.
         """
-        new_mgmtclass = mgmtclass.Mgmtclass(api)
-        new_mgmtclass.from_dict(item_dict)
-        return new_mgmtclass
+        return mgmtclass.Mgmtclass(self.api, **seed_data)
 
     def remove(
         self,
-        name,
+        name: str,
         with_delete: bool = True,
         with_sync: bool = True,
         with_triggers: bool = True,
diff --git a/cobbler/cobbler_collections/packages.py b/cobbler/cobbler_collections/packages.py
index 5118e0dbd6..ba4edb74ba 100644
--- a/cobbler/cobbler_collections/packages.py
+++ b/cobbler/cobbler_collections/packages.py
@@ -5,11 +5,16 @@
 # SPDX-License-Identifier: GPL-2.0-or-later
 # SPDX-FileCopyrightText: Copyright 2010, Kelsey Hightower <kelsey.hightower@gmail.com>
 
+from typing import TYPE_CHECKING, Any, Dict
+
 from cobbler.cobbler_collections import collection
-from cobbler.items import package as package
+from cobbler.items import package
 from cobbler import utils
 from cobbler.cexceptions import CX
 
+if TYPE_CHECKING:
+    from cobbler.api import CobblerAPI
+
 
 class Packages(collection.Collection):
     """
@@ -24,17 +29,19 @@ def collection_type() -> str:
     def collection_types() -> str:
         return "packages"
 
-    def factory_produce(self, api, item_dict):
+    def factory_produce(self, api: "CobblerAPI", seed_data: Dict[str, Any]):
         """
-        Return a Package forged from item_dict
+        Return a Package forged from seed_data.
+
+        :param api: Parameter is skipped.
+        :param seed_data: Data to seed the object with.
+        :returns: The created object.
         """
-        new_package = package.Package(api)
-        new_package.from_dict(item_dict)
-        return new_package
+        return package.Package(self.api, **seed_data)
 
     def remove(
         self,
-        name,
+        name: str,
         with_delete: bool = True,
         with_sync: bool = True,
         with_triggers: bool = True,
diff --git a/cobbler/cobbler_collections/profiles.py b/cobbler/cobbler_collections/profiles.py
index 5e553b2cd1..487c778f17 100644
--- a/cobbler/cobbler_collections/profiles.py
+++ b/cobbler/cobbler_collections/profiles.py
@@ -6,11 +6,15 @@
 # SPDX-FileCopyrightText: Copyright 2006-2009, Red Hat, Inc and Others
 # SPDX-FileCopyrightText: Michael DeHaan <michael.dehaan AT gmail>
 
+from typing import TYPE_CHECKING, Any, Dict
 from cobbler.cobbler_collections import collection
-from cobbler.items import profile as profile
+from cobbler.items import profile
 from cobbler import utils
 from cobbler.cexceptions import CX
 
+if TYPE_CHECKING:
+    from cobbler.api import CobblerAPI
+
 
 class Profiles(collection.Collection):
     """
@@ -25,13 +29,11 @@ def collection_type() -> str:
     def collection_types() -> str:
         return "profiles"
 
-    def factory_produce(self, api, item_dict):
+    def factory_produce(self, api: "CobblerAPI", seed_data: Dict[str, Any]):
         """
-        Return a Distro forged from item_dict
+        Return a Distro forged from seed_data
         """
-        new_profile = profile.Profile(api)
-        new_profile.from_dict(item_dict)
-        return new_profile
+        return profile.Profile(self.api, **seed_data)
 
     def remove(
         self,
diff --git a/cobbler/cobbler_collections/repos.py b/cobbler/cobbler_collections/repos.py
index 7c787b932b..5500233114 100644
--- a/cobbler/cobbler_collections/repos.py
+++ b/cobbler/cobbler_collections/repos.py
@@ -7,6 +7,7 @@
 # SPDX-FileCopyrightText: Michael DeHaan <michael.dehaan AT gmail>
 
 import os.path
+from typing import TYPE_CHECKING, Any, Dict
 
 from cobbler.cobbler_collections import collection
 from cobbler.items import repo
@@ -14,6 +15,9 @@
 from cobbler.cexceptions import CX
 from cobbler.utils import filesystem_helpers
 
+if TYPE_CHECKING:
+    from cobbler.api import CobblerAPI
+
 
 class Repos(collection.Collection):
     """
@@ -30,17 +34,19 @@ def collection_type() -> str:
     def collection_types() -> str:
         return "repos"
 
-    def factory_produce(self, api, item_dict):
+    def factory_produce(self, api: "CobblerAPI", seed_data: Dict[str, Any]):
         """
-        Return a Distro forged from item_dict
+        Return a Distro forged from seed_data
+
+        :param api: Parameter is skipped.
+        :param seed_data: The data the object is initalized with.
+        :returns: The created repository.
         """
-        new_repo = repo.Repo(api)
-        new_repo.from_dict(item_dict)
-        return new_repo
+        return repo.Repo(self.api, **seed_data)
 
     def remove(
         self,
-        name,
+        name: str,
         with_delete: bool = True,
         with_sync: bool = True,
         with_triggers: bool = True,
diff --git a/cobbler/cobbler_collections/systems.py b/cobbler/cobbler_collections/systems.py
index 2f87847a08..7089b4fe5a 100644
--- a/cobbler/cobbler_collections/systems.py
+++ b/cobbler/cobbler_collections/systems.py
@@ -6,11 +6,15 @@
 # SPDX-FileCopyrightText: Copyright 2008-2009, Red Hat, Inc and Others
 # SPDX-FileCopyrightText: Michael DeHaan <michael.dehaan AT gmail>
 
+from typing import TYPE_CHECKING, Any, Dict
 from cobbler.cobbler_collections import collection
-from cobbler.items import system as system
+from cobbler.items import system
 from cobbler import utils
 from cobbler.cexceptions import CX
 
+if TYPE_CHECKING:
+    from cobbler.api import CobblerAPI
+
 
 class Systems(collection.Collection):
     """
@@ -26,17 +30,17 @@ def collection_type() -> str:
     def collection_types() -> str:
         return "systems"
 
-    def factory_produce(self, api, item_dict):
+    def factory_produce(
+        self, api: "CobblerAPI", seed_data: Dict[str, Any]
+    ) -> system.System:
         """
-        Return a Distro forged from item_dict
+        Return a Distro forged from seed_data
 
-        :param api: TODO
-        :param item_dict: TODO
-        :returns: TODO
+        :param api: Parameter is skipped.
+        :param seed_data: Data to seed the object with.
+        :returns: The created object.
         """
-        new_system = system.System(api)
-        new_system.from_dict(item_dict)
-        return new_system
+        return system.System(self.api, **seed_data)
 
     def remove(
         self,
diff --git a/cobbler/cobblerd.py b/cobbler/cobblerd.py
index 903e768bed..4a091f56a4 100644
--- a/cobbler/cobblerd.py
+++ b/cobbler/cobblerd.py
@@ -7,6 +7,7 @@
 # SPDX-FileCopyrightText: Michael DeHaan <michael.dehaan AT gmail>
 
 import binascii
+import logging
 import logging.config
 import os
 import pwd
@@ -55,7 +56,7 @@ def regen_ss_file():
     os.lchown("/var/lib/cobbler/web.ss", pwd.getpwnam(http_user)[2], -1)
 
 
-def do_xmlrpc_rw(cobbler_api: CobblerAPI, port):
+def do_xmlrpc_rw(cobbler_api: CobblerAPI, port: int):
     """
     This trys to bring up the Cobbler xmlrpc_api and restart it if it fails.
 
diff --git a/cobbler/decorator.py b/cobbler/decorator.py
index 0c2e8677fe..3f99132c4a 100644
--- a/cobbler/decorator.py
+++ b/cobbler/decorator.py
@@ -3,8 +3,28 @@
 """
 # The idea for the subclassed property decorators is from: https://stackoverflow.com/a/59313599/4730773
 
+from typing import Any, Optional
+from cobbler.items import item
 
-class InheritableProperty(property):
+
+class LazyProperty(property):
+    """
+    This property is supposed to provide a way to override the lazy-read value getter.
+    """
+
+    def __get__(self, obj: Any, objtype: Optional[type] = None):
+        if obj is None:
+            return self
+        if (
+            isinstance(obj, item.Item)
+            and not obj.inmemory
+            and obj._has_initialized  # pyright: ignore [reportPrivateUsage]
+        ):
+            obj.deserialize()
+        return self.fget(obj)
+
+
+class InheritableProperty(LazyProperty):
     """
     This property is supposed to provide a way to identify properties in code that can be set to inherit.
     """
diff --git a/cobbler/items/distro.py b/cobbler/items/distro.py
index 4947826b3d..958d8c3953 100644
--- a/cobbler/items/distro.py
+++ b/cobbler/items/distro.py
@@ -7,8 +7,8 @@
 # SPDX-FileCopyrightText: Michael DeHaan <michael.dehaan AT gmail>
 import glob
 import os
-import uuid
-from typing import List, Union
+import copy
+from typing import TYPE_CHECKING, Any, List, Union
 
 from cobbler import enums, validate
 from cobbler.items import item
@@ -16,7 +16,10 @@
 from cobbler.utils import input_converters, signatures
 from cobbler.cexceptions import CX
 from cobbler import grub
-from cobbler.decorator import InheritableProperty
+from cobbler.decorator import LazyProperty, InheritableProperty
+
+if TYPE_CHECKING:
+    from cobbler.api import CobblerAPI
 
 
 class Distro(item.Item):
@@ -28,21 +31,19 @@ class Distro(item.Item):
     TYPE_NAME = "distro"
     COLLECTION_TYPE = "distro"
 
-    def __init__(self, api, *args, **kwargs):
+    def __init__(self, api: "CobblerAPI", *args: Any, **kwargs: Any):
         """
         This creates a Distro object.
 
         :param api: The Cobbler API object which is used for resolving information.
-        :param args: Place for extra parameters in this distro object.
-        :param kwargs: Place for extra parameters in this distro object.
         """
-        super().__init__(api, *args, **kwargs)
+        super().__init__(api)
         # Prevent attempts to clear the to_dict cache before the object is initialized.
         self._has_initialized = False
 
         self._tree_build_time = 0.0
         self._arch = enums.Archs.X86_64
-        self._boot_loaders: Union[list, str] = enums.VALUE_INHERITED
+        self._boot_loaders: Union[List[str], str] = enums.VALUE_INHERITED
         self._breed = ""
         self._initrd = ""
         self._kernel = ""
@@ -56,10 +57,13 @@ def __init__(self, api, *args, **kwargs):
         self._remote_boot_initrd = ""
         self._remote_grub_initrd = ""
         self._supported_boot_loaders: List[str] = []
+
+        if len(kwargs) > 0:
+            self.from_dict(kwargs)
         if not self._has_initialized:
             self._has_initialized = True
 
-    def __getattr__(self, name):
+    def __getattr__(self, name: str) -> Any:
         if name == "ks_meta":
             return self.autoinstall_meta
         raise AttributeError(f'Attribute "{name}" did not exist on object type Distro.')
@@ -75,15 +79,12 @@ def make_clone(self):
         :return: The cloned object. Not persisted on the disk or in a database.
         """
         # FIXME: Change unique base attributes
-        _dict = self.to_dict()
+        _dict = copy.deepcopy(self.to_dict())
         # Drop attributes which are computed from other attributes
-        computed_properties = ["remote_grub_initrd", "remote_grub_kernel"]
+        computed_properties = ["remote_grub_initrd", "remote_grub_kernel", "uid"]
         for property_name in computed_properties:
             _dict.pop(property_name, None)
-        cloned = Distro(self.api)
-        cloned.from_dict(_dict)
-        cloned.uid = uuid.uuid4().hex
-        return cloned
+        return Distro(self.api, **_dict)
 
     @classmethod
     def _remove_depreacted_dict_keys(cls, dictionary: dict):
@@ -96,22 +97,13 @@ def _remove_depreacted_dict_keys(cls, dictionary: dict):
             dictionary.pop("parent")
         super()._remove_depreacted_dict_keys(dictionary)
 
-    def from_dict(self, dictionary: dict):
-        """
-        Initializes the object with attributes from the dictionary.
-
-        :param dictionary: The dictionary with values.
-        """
-        if "name" in dictionary:
-            self.name = dictionary["name"]
-        self._remove_depreacted_dict_keys(dictionary)
-        super().from_dict(dictionary)
-
     def check_if_valid(self):
         """
         Check if a distro object is valid. If invalid an exception is raised.
         """
         super().check_if_valid()
+        if not self.inmemory:
+            return
         if self.kernel is None:
             raise CX(f"Error with distro {self.name} - kernel is required")
 
@@ -137,7 +129,7 @@ def parent(self, value):
             "Setting the parent of a distribution is not supported. Ignoring action!"
         )
 
-    @property
+    @LazyProperty
     def kernel(self) -> str:
         """
         Specifies a kernel. The kernel parameter is a full path, a filename in the configured kernel directory or a
@@ -167,7 +159,7 @@ def kernel(self, kernel: str):
             )
         self._kernel = kernel
 
-    @property
+    @LazyProperty
     def remote_boot_kernel(self) -> str:
         """
         URL to a remote kernel. If the bootloader supports this feature, it directly tries to retrieve the kernel and
@@ -208,7 +200,7 @@ def remote_boot_kernel(self, remote_boot_kernel: str):
         self._remote_grub_kernel = parsed_url
         self._remote_boot_kernel = remote_boot_kernel
 
-    @property
+    @LazyProperty
     def tree_build_time(self) -> float:
         """
         Represents the import time of the distro. If not imported, this field is not meaningful.
@@ -233,7 +225,7 @@ def tree_build_time(self, datestamp: float):
             raise TypeError("datestamp needs to be of type float")
         self._tree_build_time = datestamp
 
-    @property
+    @LazyProperty
     def breed(self) -> str:
         """
         The repository system breed. This decides some defaults for most actions with a repo in Cobbler.
@@ -252,7 +244,7 @@ def breed(self, breed: str):
         """
         self._breed = validate.validate_breed(breed)
 
-    @property
+    @LazyProperty
     def os_version(self) -> str:
         r"""
         The operating system version which the image contains.
@@ -271,9 +263,9 @@ def os_version(self, os_version: str):
         """
         self._os_version = validate.validate_os_version(os_version, self.breed)
 
-    @property
+    @LazyProperty
     def initrd(self) -> str:
-        """
+        r"""
         Specifies an initrd image. Path search works as in set_kernel. File must be named appropriately.
 
         :getter: The current path to the initrd.
@@ -299,9 +291,9 @@ def initrd(self, initrd: str):
             return
         raise ValueError("initrd not found")
 
-    @property
+    @LazyProperty
     def remote_grub_kernel(self) -> str:
-        """
+        r"""
         This is tied to the ``remote_boot_kernel`` property. It contains the URL of that field in a format which grub
         can use directly.
 
@@ -309,7 +301,7 @@ def remote_grub_kernel(self) -> str:
         """
         return self._remote_grub_kernel
 
-    @property
+    @LazyProperty
     def remote_grub_initrd(self) -> str:
         r"""
         This is tied to the ``remote_boot_initrd`` property. It contains the URL of that field in a format which grub
@@ -319,7 +311,7 @@ def remote_grub_initrd(self) -> str:
         """
         return self._remote_grub_initrd
 
-    @property
+    @LazyProperty
     def remote_boot_initrd(self) -> str:
         r"""
         URL to a remote initrd. If the bootloader supports this feature, it directly tries to retrieve the initrd and
@@ -332,7 +324,7 @@ def remote_boot_initrd(self) -> str:
 
     @remote_boot_initrd.setter
     def remote_boot_initrd(self, remote_boot_initrd: str):
-        """
+        r"""
         The setter for the ``remote_boot_initrd`` property.
 
         :param remote_boot_initrd: The new value for the property.
@@ -357,9 +349,9 @@ def remote_boot_initrd(self, remote_boot_initrd: str):
         self._remote_grub_initrd = parsed_url
         self._remote_boot_initrd = remote_boot_initrd
 
-    @property
+    @LazyProperty
     def source_repos(self) -> list:
-        """
+        r"""
         A list of http:// URLs on the Cobbler server that point to yum configuration files that can be used to
         install core packages. Use by ``cobbler import`` only.
 
@@ -382,7 +374,7 @@ def source_repos(self, repos: list):
             )
         self._source_repos = repos
 
-    @property
+    @LazyProperty
     def arch(self):
         """
         The field is mainly relevant to PXE provisioning.
@@ -422,7 +414,7 @@ def supported_boot_loaders(self) -> List[str]:
 
     @InheritableProperty
     def boot_loaders(self) -> list:
-        """
+        r"""
         All boot loaders for which Cobbler generates entries for.
 
         .. note:: This property can be set to ``<<inherit>>``.
@@ -463,7 +455,7 @@ def boot_loaders(self, boot_loaders: List[str]):
 
     @InheritableProperty
     def redhat_management_key(self) -> str:
-        """
+        r"""
         Get the redhat management key. This is probably only needed if you have spacewalk, uyuni or SUSE Manager
         running.
 
@@ -488,7 +480,7 @@ def redhat_management_key(self, management_key: str):
         self._redhat_management_key = management_key
 
     def find_distro_path(self):
-        """
+        r"""
         This returns the absolute path to the distro under the ``distro_mirror`` directory. If that directory doesn't
         contain the kernel, the directory of the kernel in the distro is returned.
 
diff --git a/cobbler/items/file.py b/cobbler/items/file.py
index 33f3ecd422..65a8ea3726 100644
--- a/cobbler/items/file.py
+++ b/cobbler/items/file.py
@@ -6,13 +6,17 @@
 # SPDX-FileCopyrightText: Copyright 2006-2009, Red Hat, Inc and Others
 # SPDX-FileCopyrightText: Kelsey Hightower <kelsey.hightower@gmail.com>
 
-import uuid
+import copy
+from typing import TYPE_CHECKING, Any
 
 from cobbler.utils import input_converters
 from cobbler.items import resource
-
+from cobbler.decorator import LazyProperty
 from cobbler.cexceptions import CX
 
+if TYPE_CHECKING:
+    from cobbler.api import CobblerAPI
+
 
 class File(resource.Resource):
     """
@@ -22,19 +26,20 @@ class File(resource.Resource):
     TYPE_NAME = "file"
     COLLECTION_TYPE = "file"
 
-    def __init__(self, api, *args, **kwargs):
+    def __init__(self, api: "CobblerAPI", *args: Any, **kwargs: Any):
         """
         Constructor.
 
         :param api: The Cobbler API object which is used for resolving information.
-        :param args: The arguments which should be passed additionally to a Resource.
-        :param kwargs: The keyword arguments which should be passed additionally to a Resource.
         """
-        super().__init__(api, *args, **kwargs)
+        super().__init__(api)
         # Prevent attempts to clear the to_dict cache before the object is initialized.
         self._has_initialized = False
 
         self._is_dir = False
+
+        if len(kwargs) > 0:
+            self.from_dict(kwargs)
         if not self._has_initialized:
             self._has_initialized = True
 
@@ -48,20 +53,9 @@ def make_clone(self):
 
         :return: The cloned instance of this object.
         """
-        _dict = self.to_dict()
-        cloned = File(self.api)
-        cloned.from_dict(_dict)
-        cloned.uid = uuid.uuid4().hex
-        return cloned
-
-    def from_dict(self, dictionary: dict):
-        """
-        Initializes the object with attributes from the dictionary.
-
-        :param dictionary: The dictionary with values.
-        """
-        self._remove_depreacted_dict_keys(dictionary)
-        super().from_dict(dictionary)
+        _dict = copy.deepcopy(self.to_dict())
+        _dict.pop("uid", None)
+        return File(self.api, **_dict)
 
     def check_if_valid(self):
         """
@@ -71,6 +65,8 @@ def check_if_valid(self):
         :raises CX: Raised in case a required argument is missing
         """
         super().check_if_valid()
+        if not self.inmemory:
+            return
         if not self.path:
             raise CX("path is required")
         if not self.owner:
@@ -86,9 +82,9 @@ def check_if_valid(self):
     # specific methods for item.File
     #
 
-    @property
+    @LazyProperty
     def is_dir(self):
-        """
+        r"""
         Is this a directory or not.
 
         :getter: Returns the value of ``is_dir``
@@ -98,7 +94,7 @@ def is_dir(self):
 
     @is_dir.setter
     def is_dir(self, is_dir: bool):
-        """
+        r"""
         If true, treat file resource as a directory. Templates are ignored.
 
         :param is_dir: This is the path to check if it is a directory.
diff --git a/cobbler/items/image.py b/cobbler/items/image.py
index ef8a1c7347..6299041a04 100644
--- a/cobbler/items/image.py
+++ b/cobbler/items/image.py
@@ -6,13 +6,13 @@
 # SPDX-FileCopyrightText: Copyright 2006-2009, Red Hat, Inc and Others
 # SPDX-FileCopyrightText: Michael DeHaan <michael.dehaan AT gmail>
 
-import uuid
-from typing import TYPE_CHECKING, List, Union
+from typing import TYPE_CHECKING, List, Union, Any
+import copy
 
 from cobbler import autoinstall_manager, enums, validate
 from cobbler.cexceptions import CX
 from cobbler.items import item
-from cobbler.decorator import InheritableProperty
+from cobbler.decorator import LazyProperty, InheritableProperty
 from cobbler.utils import input_converters, signatures
 
 if TYPE_CHECKING:
@@ -27,15 +27,13 @@ class Image(item.Item):
     TYPE_NAME = "image"
     COLLECTION_TYPE = "image"
 
-    def __init__(self, api: "CobblerAPI", *args, **kwargs):
+    def __init__(self, api: "CobblerAPI", *args: Any, **kwargs: Any):
         """
         Constructor
 
         :param api: The Cobbler API object which is used for resolving information.
-        :param args: The arguments which should be passed additionally to the base Item class constructor.
-        :param kwargs: The keyword arguments which should be passed additionally to the base Item class constructor.
         """
-        super().__init__(api, *args, **kwargs)
+        super().__init__(api)
         # Prevent attempts to clear the to_dict cache before the object is initialized.
         self._has_initialized = False
 
@@ -58,7 +56,10 @@ def __init__(self, api: "CobblerAPI", *args, **kwargs):
         self._virt_path = ""
         self._virt_ram: Union[str, int] = enums.VALUE_INHERITED
         self._virt_type: Union[str, enums.VirtType] = enums.VirtType.INHERITED
-        self._supported_boot_loaders = []
+        self._supported_boot_loaders: List[str] = []
+
+        if len(kwargs):
+            self.from_dict(kwargs)
         if not self._has_initialized:
             self._has_initialized = True
 
@@ -77,30 +78,15 @@ def make_clone(self):
 
         :return: The cloned instance of this object.
         """
-        _dict = self.to_dict()
-        cloned = Image(self.api)
-        cloned.from_dict(_dict)
-        cloned.uid = uuid.uuid4().hex
-        return cloned
-
-    def from_dict(self, dictionary: dict):
-        """
-        Initializes the object with attributes from the dictionary.
-
-        :param dictionary: The dictionary with values.
-        """
-        if "name" in dictionary:
-            self.name = dictionary["name"]
-        if "parent" in dictionary:
-            self.parent = dictionary["parent"]
-        self._remove_depreacted_dict_keys(dictionary)
-        super().from_dict(dictionary)
+        _dict = copy.deepcopy(self.to_dict())
+        _dict.pop("uid", None)
+        return Image(self.api, **_dict)
 
     #
     # specific methods for item.Image
     #
 
-    @property
+    @LazyProperty
     def arch(self) -> enums.Archs:
         """
         Represents the architecture the image has. If deployed to a physical host this should be enforced, a virtual
@@ -150,7 +136,7 @@ def autoinstall(self, autoinstall: str):
             autoinstall
         )
 
-    @property
+    @LazyProperty
     def file(self) -> str:
         """
         Stores the image location. This should be accessible on all nodes that need to access it.
@@ -214,7 +200,7 @@ def file(self, filename: str):
 
         self._file = uri
 
-    @property
+    @LazyProperty
     def os_version(self) -> str:
         r"""
         The operating system version which the image contains.
@@ -233,7 +219,7 @@ def os_version(self, os_version):
         """
         self._os_version = validate.validate_os_version(os_version, self.breed)
 
-    @property
+    @LazyProperty
     def breed(self) -> str:
         r"""
         The operating system breed.
@@ -252,7 +238,7 @@ def breed(self, breed: str):
         """
         self._breed = validate.validate_breed(breed)
 
-    @property
+    @LazyProperty
     def image_type(self) -> enums.ImageTypes:
         """
         Indicates what type of image this is.
@@ -296,7 +282,7 @@ def image_type(self, image_type: Union[enums.ImageTypes, str]):
             )
         self._image_type = image_type
 
-    @property
+    @LazyProperty
     def virt_cpus(self) -> int:
         """
         The amount of vCPU cores used in case the image is being deployed on top of a VM host.
@@ -315,7 +301,7 @@ def virt_cpus(self, num: int):
         """
         self._virt_cpus = validate.validate_virt_cpus(num)
 
-    @property
+    @LazyProperty
     def network_count(self) -> int:
         """
         Represents the number of virtual NICs this image has.
@@ -464,7 +450,7 @@ def virt_bridge(self, vbridge: str):
         """
         self._virt_bridge = validate.validate_virt_bridge(vbridge)
 
-    @property
+    @LazyProperty
     def virt_path(self) -> str:
         """
         Represents the location where the image for the VM is stored.
@@ -483,7 +469,7 @@ def virt_path(self, path: str):
         """
         self._virt_path = validate.validate_virt_path(path)
 
-    @property
+    @LazyProperty
     def menu(self) -> str:
         """
         Property to represent the menu which this image should be put into.
@@ -507,7 +493,7 @@ def menu(self, menu: str):
                 raise CX(f"menu {menu} not found")
         self._menu = menu
 
-    @property
+    @LazyProperty
     def display_name(self) -> str:
         """
         Returns the display name.
@@ -549,10 +535,10 @@ def boot_loaders(self) -> List[str]:
         """
         if self._boot_loaders == enums.VALUE_INHERITED:
             return self.supported_boot_loaders
-        return self._boot_loaders
+        return self._boot_loaders  # type: ignore
 
     @boot_loaders.setter
-    def boot_loaders(self, boot_loaders: list):
+    def boot_loaders(self, boot_loaders: Union[str, List[str]]):
         """
         Setter of the boot loaders.
 
diff --git a/cobbler/items/item.py b/cobbler/items/item.py
index 42ee9f4c87..45e6ef5c42 100644
--- a/cobbler/items/item.py
+++ b/cobbler/items/item.py
@@ -20,7 +20,7 @@
 from cobbler import utils, enums
 from cobbler.utils import input_converters
 from cobbler.cexceptions import CX
-from cobbler.decorator import InheritableProperty, InheritableDictProperty
+from cobbler.decorator import LazyProperty, InheritableProperty, InheritableDictProperty
 
 if TYPE_CHECKING:
     from cobbler.api import CobblerAPI
@@ -34,7 +34,7 @@ class ItemCache:
     A Cobbler ItemCache object.
     """
 
-    def __init__(self, api):
+    def __init__(self, api: "CobblerAPI"):
         """
         Constructor
 
@@ -49,12 +49,15 @@ def __init__(self, api):
          {True:  cache_value or None,
           False: cache_value or None}
         """
-        self._cached_dict: Dict[bool, Optional[Dict]] = {True: None, False: None}
+        self._cached_dict: Dict[bool, Optional[Dict[str, Any]]] = {
+            True: None,
+            False: None,
+        }
 
         self.api = api
         self.settings = api.settings()
 
-    def get_dict_cache(self, resolved: bool) -> Optional[Dict]:
+    def get_dict_cache(self, resolved: bool) -> Optional[Dict[str, Any]]:
         """
         Gettinging the dict cache.
 
@@ -65,7 +68,7 @@ def get_dict_cache(self, resolved: bool) -> Optional[Dict]:
             return self._cached_dict[resolved]
         return None
 
-    def set_dict_cache(self, value: Optional[Dict], resolved: bool):
+    def set_dict_cache(self, value: Optional[Dict[str, Any]], resolved: bool):
         """
         Setter for the dict cache.
 
@@ -224,14 +227,14 @@ def __find_compare(
         raise TypeError(f"find cannot compare type: {type(from_obj)}")
 
     @staticmethod
-    def __is_dict_key(name) -> bool:
+    def __is_dict_key(name: str) -> bool:
         """
         Whether the attribute is part of the item's to_dict or not
 
         :name: The attribute name.
         """
         return (
-            name.startswith("_")
+            name[:1] == "_"
             and "__" not in name
             and name
             not in {
@@ -239,10 +242,11 @@ def __is_dict_key(name) -> bool:
                 "_cache",
                 "_supported_boot_loaders",
                 "_has_initialized",
+                "_inmemory",
             }
         )
 
-    def __init__(self, api: "CobblerAPI", is_subobject: bool = False):
+    def __init__(self, api: "CobblerAPI", is_subobject: bool = False, **kwargs: Any):
         """
         Constructor.  Requires a back reference to the CobblerAPI object.
 
@@ -257,6 +261,8 @@ def __init__(self, api: "CobblerAPI", is_subobject: bool = False):
                                profile
                                     profile  <-- created with is_subobject=True
                                          system   <-- created as normal
+                           image
+                               system
                            menu
                                menu
 
@@ -265,6 +271,9 @@ def __init__(self, api: "CobblerAPI", is_subobject: bool = False):
         objects of the same type (i.e. subprofiles) need to pass this in as True.  Otherwise, just
         use False for is_subobject and the parent object will (therefore) have a different type.
 
+        The keyword arguments are used to seed the object. This is the preferred way over ``from_dict`` starting with
+        Cobbler version 3.4.0.
+
         :param api: The Cobbler API object which is used for resolving information.
         :param is_subobject: See above extensive description.
         """
@@ -290,13 +299,21 @@ def __init__(self, api: "CobblerAPI", is_subobject: bool = False):
         self._mgmt_classes: Union[list, str] = enums.VALUE_INHERITED
         self._mgmt_parameters: Union[dict, str] = {}
         self._is_subobject = is_subobject
+        self._inmemory = True
 
         self.logger = logging.getLogger()
         self.api = api
+
+        if len(kwargs) > 0:
+            kwargs.update({"is_subobject": is_subobject})
+            Item.from_dict(self, kwargs)
+        if self._uid == "":
+            self._uid = uuid.uuid4().hex
+
         if not self._has_initialized:
             self._has_initialized = True
 
-    def __eq__(self, other):
+    def __eq__(self, other: Any):
         """
         Comparison based on the uid for our items.
 
@@ -310,6 +327,8 @@ def __eq__(self, other):
     def __hash__(self):
         """
         Hash table for Items.
+        Requires special handling if the uid value changes and the Item
+        is present in set, frozenset, and dict types.
 
         :return: hash(uid).
         """
@@ -322,7 +341,7 @@ def __setattr__(self, name, value):
         :name: The attribute name.
         :value: The attribute value.
         """
-        if self.__is_dict_key(name) and self._has_initialized:
+        if Item.__is_dict_key(name) and self._has_initialized:
             self.clean_cache(name)
         super().__setattr__(name, value)
 
@@ -453,6 +472,16 @@ def uid(self, uid: str):
 
         :param uid: The new uid.
         """
+        if self._uid != uid and self.COLLECTION_TYPE != Item.COLLECTION_TYPE:
+            name = self.name.lower()
+            collection = self.api.get_items(self.COLLECTION_TYPE)
+            with collection.lock:
+                if collection.get(name) is not None:
+                    # Changing the hash of an object requires special handling.
+                    collection.listing.pop(name)
+                    self._uid = uid
+                    collection.listing[name] = self
+                    return
         self._uid = uid
 
     @property
@@ -503,7 +532,7 @@ def name(self, name: str):
             raise ValueError(f"Invalid characters in name: '{name}'")
         self._name = name
 
-    @property
+    @LazyProperty
     def comment(self) -> str:
         """
         For every object you are able to set a unique comment which will be persisted on the object.
@@ -688,7 +717,7 @@ def mgmt_parameters(self, mgmt_parameters: Union[str, dict]):
                 )
         self._mgmt_parameters = mgmt_parameters
 
-    @property
+    @LazyProperty
     def template_files(self) -> dict:
         """
         File mappings for built-in configuration management
@@ -714,7 +743,7 @@ def template_files(self, template_files: dict):
         except TypeError as error:
             raise TypeError("invalid template files specified") from error
 
-    @property
+    @LazyProperty
     def boot_files(self) -> dict:
         """
         Files copied into tftpboot beyond the kernel/initrd
@@ -768,7 +797,7 @@ def fetchable_files(self, fetchable_files: Union[str, dict]):
         except TypeError as error:
             raise TypeError("invalid fetchable files specified") from error
 
-    @property
+    @LazyProperty
     def depth(self) -> int:
         """
         This represents the logical depth of an object in the category of the same items. Important for the order of
@@ -811,8 +840,8 @@ def mtime(self, mtime: float):
             raise TypeError("mtime needs to be of type float")
         self._mtime = mtime
 
-    @property
-    def parent(self) -> Any:
+    @LazyProperty
+    def parent(self):
         """
         This property contains the name of the parent of an object. In case there is not parent this return
         None.
@@ -845,6 +874,7 @@ def parent(self, parent: str):
         self._parent = parent
         self.depth = found.depth + 1
 
+    @LazyProperty
     def get_parent(self) -> str:
         """
         This method returns the name of the parent for the object. In case there is not parent this return
@@ -852,7 +882,7 @@ def get_parent(self) -> str:
         """
         return self._parent
 
-    def get_conceptual_parent(self) -> Any:
+    def get_conceptual_parent(self):
         """
         The parent may just be a superclass for something like a subprofile. Get the first parent of a different type.
 
@@ -904,7 +934,7 @@ def children(self) -> List[Any]:
         results = []
         list_items = self.api.get_items(self.COLLECTION_TYPE)
         for obj in list_items:
-            if obj.get_parent() == self._name:
+            if obj.get_parent == self._name:
                 results.append(obj)
         return results
 
@@ -943,7 +973,7 @@ def descendants(self) -> list:
                     results.update(dep_item.descendants)
         return list(results)
 
-    @property
+    @LazyProperty
     def is_subobject(self) -> bool:
         """
         Weather the object is a subobject of another object or not.
@@ -1090,7 +1120,7 @@ def make_clone(self):
         raise NotImplementedError("Must be implemented in a specific Item")
 
     @classmethod
-    def _remove_depreacted_dict_keys(cls, dictionary: dict):
+    def _remove_depreacted_dict_keys(cls, dictionary: Dict[str, Any]):
         """
         This method does remove keys which should not be deserialized and are only there for API compatibility in
         :meth:`~cobbler.items.item.Item.to_dict`.
@@ -1104,7 +1134,7 @@ def _remove_depreacted_dict_keys(cls, dictionary: dict):
         if "children" in dictionary:
             dictionary.pop("children")
 
-    def from_dict(self, dictionary: dict):
+    def from_dict(self, dictionary: Dict[str, Any]):
         """
         Modify this object to take on values in ``dictionary``.
 
@@ -1112,6 +1142,11 @@ def from_dict(self, dictionary: dict):
         :raises AttributeError: In case during the process of setting a value for an attribute an error occurred.
         :raises KeyError: In case there were keys which could not be set in the item dictionary.
         """
+        self._remove_depreacted_dict_keys(dictionary)
+        if len(dictionary) == 0:
+            return
+        old_has_initialized = self._has_initialized
+        self._has_initialized = False
         result = copy.deepcopy(dictionary)
         for key in dictionary:
             lowered_key = key.lower()
@@ -1125,6 +1160,8 @@ def from_dict(self, dictionary: dict):
                         f'Attribute "{lowered_key}" could not be set!'
                     ) from error
                 result.pop(key)
+        self._has_initialized = old_has_initialized
+        self.clean_cache()
         if len(result) > 0:
             raise KeyError(
                 f"The following keys supplied could not be set: {result.keys()}"
@@ -1138,6 +1175,8 @@ def to_dict(self, resolved: bool = False) -> Dict[str, Any]:
                      objects raw value.
         :return: A dictionary with all values present in this object.
         """
+        if not self.inmemory:
+            self.deserialize()
         cached_result = self.cache.get_dict_cache(resolved)
         if cached_result is not None:
             return cached_result
@@ -1182,7 +1221,7 @@ def to_dict(self, resolved: bool = False) -> Dict[str, Any]:
         self.cache.set_dict_cache(value, resolved)
         return value
 
-    def serialize(self) -> dict:
+    def serialize(self) -> Dict[str, Any]:
         """
         This method is a proxy for :meth:`~cobbler.items.item.Item.to_dict` and contains additional logic for
         serialization to a persistent location.
@@ -1200,12 +1239,31 @@ def serialize(self) -> dict:
             result.pop(key, "")
         return result
 
-    def deserialize(self, item_dict: dict):
-        """
-        This is currently a proxy for :py:meth:`~cobbler.items.item.Item.from_dict` .
-
-        :param item_dict: The dictionary with the data to deserialize.
-        """
+    def deserialize(self):
+        """
+        Deserializes the object itself and, if necessary, recursively all the objects it depends on.
+        """
+
+        def deserialize_ancestor(ancestor_item_type: str, ancestor_name: str):
+            if ancestor_name not in {"", enums.VALUE_INHERITED}:
+                ancestor = self.api.get_items(ancestor_item_type).get(ancestor_name)
+                if ancestor is not None and not ancestor.inmemory:
+                    ancestor.deserialize()
+
+        item_dict = self.api.deserialize_item(self)
+        if item_dict["inmemory"]:
+            for ancestor_item_type, ancestor_deps in Item.TYPE_DEPENDENCIES.items():
+                for ancestor_dep in ancestor_deps:
+                    if self.TYPE_NAME == ancestor_dep[0]:
+                        attr_name = ancestor_dep[1]
+                        if attr_name not in item_dict:
+                            continue
+                        attr_val = item_dict[attr_name]
+                        if isinstance(attr_val, str):
+                            deserialize_ancestor(ancestor_item_type, attr_val)
+                        elif isinstance(attr_val, list):
+                            for ancestor_name in attr_val:
+                                deserialize_ancestor(ancestor_item_type, ancestor_name)
         self.from_dict(item_dict)
 
     def grab_tree(self) -> list:
@@ -1250,7 +1308,7 @@ def _clean_dict_cache(self, name: Optional[str]):
         if not self.api.settings().cache_enabled:
             return
 
-        if name is not None:
+        if name is not None and self._inmemory:
             attr = getattr(type(self), name[1:])
             if (
                 isinstance(attr, (InheritableProperty, InheritableDictProperty))
@@ -1272,4 +1330,25 @@ def clean_cache(self, name: Optional[str] = None):
                     Can be Item, Settings or SIGNATURE_CACHE.
         :param name: The name of Item attribute or None.
         """
-        self._clean_dict_cache(name)
+        if self._inmemory:
+            self._clean_dict_cache(name)
+
+    @property
+    def inmemory(self) -> bool:
+        r"""
+        If set to ``false``, only the Item name is in memory. The rest of the Item's properties can be retrieved
+        either on demand or as a result of the ``load_items`` background task.
+
+        :getter: The inmemory for the item.
+        :setter: The new inmemory value for the object. Should only be used by the Cobbler serializers.
+        """
+        return self._inmemory
+
+    @inmemory.setter
+    def inmemory(self, inmemory: bool):
+        """
+        Setter for the inmemory of the item.
+
+        :param inmemory: The new inmemory value.
+        """
+        self._inmemory = inmemory
diff --git a/cobbler/items/menu.py b/cobbler/items/menu.py
index e081103c06..d9f5dd135d 100644
--- a/cobbler/items/menu.py
+++ b/cobbler/items/menu.py
@@ -5,11 +5,14 @@
 # SPDX-License-Identifier: GPL-2.0-or-later
 # SPDX-FileCopyrightText: Copyright 2021 Yuriy Chelpanov <yuriy.chelpanov@gmail.com>
 
-import uuid
-from typing import List, Optional
+import copy
+from typing import TYPE_CHECKING, Any
 
 from cobbler.items import item
-from cobbler.cexceptions import CX
+from cobbler.decorator import LazyProperty
+
+if TYPE_CHECKING:
+    from cobbler.api import CobblerAPI
 
 
 class Menu(item.Item):
@@ -20,15 +23,20 @@ class Menu(item.Item):
     TYPE_NAME = "menu"
     COLLECTION_TYPE = "menu"
 
-    def __init__(self, api, *args, **kwargs):
+    def __init__(self, api: "CobblerAPI", *args: Any, **kwargs: Any):
         """
         Constructor
+
+        :param api: The Cobbler API object which is used for resolving information.
         """
-        super().__init__(api, *args, **kwargs)
+        super().__init__(api)
         # Prevent attempts to clear the to_dict cache before the object is initialized.
         self._has_initialized = False
 
         self._display_name = ""
+
+        if len(kwargs) > 0:
+            self.from_dict(kwargs)
         if not self._has_initialized:
             self._has_initialized = True
 
@@ -42,26 +50,15 @@ def make_clone(self):
 
         :return: The cloned instance of this object.
         """
-        _dict = self.to_dict()
-        cloned = Menu(self.api)
-        cloned.from_dict(_dict)
-        cloned.uid = uuid.uuid4().hex
-        return cloned
-
-    def from_dict(self, dictionary: dict):
-        """
-        Initializes the object with attributes from the dictionary.
-
-        :param dictionary: The dictionary with values.
-        """
-        self._remove_depreacted_dict_keys(dictionary)
-        super().from_dict(dictionary)
+        _dict = copy.deepcopy(self.to_dict())
+        _dict.pop("uid", None)
+        return Menu(self.api, **_dict)
 
     #
     # specific methods for item.Menu
     #
 
-    @property
+    @LazyProperty
     def display_name(self) -> str:
         """
         Returns the display name.
diff --git a/cobbler/items/mgmtclass.py b/cobbler/items/mgmtclass.py
index 23ce35ee13..0588fde961 100644
--- a/cobbler/items/mgmtclass.py
+++ b/cobbler/items/mgmtclass.py
@@ -5,11 +5,15 @@
 # SPDX-License-Identifier: GPL-2.0-or-later
 # SPDX-FileCopyrightText: Copyright 2010, Kelsey Hightower <kelsey.hightower@gmail.com>
 
-import uuid
-from typing import Union
+import copy
+from typing import TYPE_CHECKING, Any, Dict, List, Union
 
 from cobbler.items import item
 from cobbler.utils import input_converters
+from cobbler.decorator import LazyProperty
+
+if TYPE_CHECKING:
+    from cobbler.api import CobblerAPI
 
 
 class Mgmtclass(item.Item):
@@ -20,23 +24,24 @@ class Mgmtclass(item.Item):
     TYPE_NAME = "mgmtclass"
     COLLECTION_TYPE = "mgmtclass"
 
-    def __init__(self, api, *args, **kwargs):
+    def __init__(self, api: "CobblerAPI", *args: Any, **kwargs: Any):
         """
         Constructor.
 
         :param api: The Cobbler API object which is used for resolving information.
-        :param args: The arguments which should be passed additionally to the base Item class constructor.
-        :param kwargs: The keyword arguments which should be passed additionally to the base Item class constructor.
         """
-        super().__init__(api, *args, **kwargs)
+        super().__init__(api)
         # Prevent attempts to clear the to_dict cache before the object is initialized.
         self._has_initialized = False
 
         self._is_definition = False
-        self._params = {}
+        self._params: Dict[str, Any] = {}
         self._class_name = ""
-        self._files = []
-        self._packages = []
+        self._files: List[str] = []
+        self._packages: List[str] = []
+
+        if len(kwargs) > 0:
+            self.from_dict(kwargs)
         if not self._has_initialized:
             self._has_initialized = True
 
@@ -51,27 +56,16 @@ def make_clone(self):
         :return: The cloned instance of this object.
         """
 
-        _dict = self.to_dict()
-        cloned = Mgmtclass(self.api)
-        cloned.from_dict(_dict)
-        cloned.uid = uuid.uuid4().hex
-        return cloned
-
-    def from_dict(self, dictionary: dict):
-        """
-        Initializes the object with attributes from the dictionary.
-
-        :param dictionary: The dictionary with values.
-        """
-        self._remove_depreacted_dict_keys(dictionary)
-        super().from_dict(dictionary)
+        _dict = copy.deepcopy(self.to_dict())
+        _dict.pop("uid", None)
+        return Mgmtclass(self.api, **_dict)
 
     #
     # specific methods for item.Mgmtclass
     #
 
-    @property
-    def packages(self) -> list:
+    @LazyProperty
+    def packages(self) -> List[str]:
         """
         Packages property.
 
@@ -81,7 +75,7 @@ def packages(self) -> list:
         return self._packages
 
     @packages.setter
-    def packages(self, packages: list):
+    def packages(self, packages: List[str]):
         """
         Setter for the packages of the management class.
 
@@ -89,8 +83,8 @@ def packages(self, packages: list):
         """
         self._packages = input_converters.input_string_or_list(packages)
 
-    @property
-    def files(self) -> list:
+    @LazyProperty
+    def files(self) -> List[str]:
         """
         Files property.
 
@@ -100,7 +94,7 @@ def files(self) -> list:
         return self._files
 
     @files.setter
-    def files(self, files: Union[str, list]):
+    def files(self, files: Union[str, List[str]]):
         """
         Setter for the files of the object.
 
@@ -108,8 +102,8 @@ def files(self, files: Union[str, list]):
         """
         self._files = input_converters.input_string_or_list(files)
 
-    @property
-    def params(self) -> dict:
+    @LazyProperty
+    def params(self) -> Dict[str, Any]:
         """
         Params property.
 
@@ -119,7 +113,7 @@ def params(self) -> dict:
         return self._params
 
     @params.setter
-    def params(self, params: dict):
+    def params(self, params: Dict[str, Any]):
         """
         Setter for the params of the management class.
 
@@ -133,7 +127,7 @@ def params(self, params: dict):
         except TypeError as error:
             raise TypeError("invalid value for params") from error
 
-    @property
+    @LazyProperty
     def is_definition(self) -> bool:
         """
         Is_definition property.
@@ -156,7 +150,7 @@ def is_definition(self, isdef: bool):
             raise TypeError("Field is_defintion from mgmtclass must be of type bool.")
         self._is_definition = isdef
 
-    @property
+    @LazyProperty
     def class_name(self) -> str:
         """
         The name of the management class.
diff --git a/cobbler/items/package.py b/cobbler/items/package.py
index 74e14a6237..38d7185ab8 100644
--- a/cobbler/items/package.py
+++ b/cobbler/items/package.py
@@ -6,9 +6,14 @@
 # SPDX-FileCopyrightText: Copyright 2006-2009, Red Hat, Inc and Others
 # SPDX-FileCopyrightText: Kelsey Hightower <kelsey.hightower@gmail.com>
 
-import uuid
+import copy
+from typing import TYPE_CHECKING, Any
 
 from cobbler.items import resource
+from cobbler.decorator import LazyProperty
+
+if TYPE_CHECKING:
+    from cobbler.api import CobblerAPI
 
 
 class Package(resource.Resource):
@@ -19,20 +24,22 @@ class Package(resource.Resource):
     TYPE_NAME = "package"
     COLLECTION_TYPE = "package"
 
-    def __init__(self, api, *args, **kwargs):
+    def __init__(self, api: "CobblerAPI", *args: Any, **kwargs: Any):
         """
         Constructor
 
         :param api: The Cobbler API object which is used for resolving information.
-        :param args: The arguments which should be passed additionally to a Resource.
-        :param kwargs: The keyword arguments which should be passed additionally to a Resource.
+        :param item_dict: Dictionary of properties to initialize the object.
         """
-        super().__init__(api, *args, **kwargs)
+        super().__init__(api)
         # Prevent attempts to clear the to_dict cache before the object is initialized.
         self._has_initialized = False
 
         self._installer = ""
         self._version = ""
+
+        if len(kwargs) > 0:
+            self.from_dict(kwargs)
         if not self._has_initialized:
             self._has_initialized = True
 
@@ -46,26 +53,15 @@ def make_clone(self):
 
         :return: The cloned instance of this object.
         """
-        _dict = self.to_dict()
-        cloned = Package(self.api)
-        cloned.from_dict(_dict)
-        cloned.uid = uuid.uuid4().hex
-        return cloned
-
-    def from_dict(self, dictionary: dict):
-        """
-        Initializes the object with attributes from the dictionary.
-
-        :param dictionary: The dictionary with values.
-        """
-        self._remove_depreacted_dict_keys(dictionary)
-        super().from_dict(dictionary)
+        _dict = copy.deepcopy(self.to_dict())
+        _dict.pop("uid", None)
+        return Package(self.api, **_dict)
 
     #
     # specific methods for item.Package
     #
 
-    @property
+    @LazyProperty
     def installer(self) -> str:
         """
         Installer property.
@@ -89,7 +85,7 @@ def installer(self, installer: str):
             )
         self._installer = installer.lower()
 
-    @property
+    @LazyProperty
     def version(self) -> str:
         """
         Version property.
diff --git a/cobbler/items/profile.py b/cobbler/items/profile.py
index eb667f1d37..e0a92754a7 100644
--- a/cobbler/items/profile.py
+++ b/cobbler/items/profile.py
@@ -6,15 +6,15 @@
 # SPDX-FileCopyrightText: Copyright 2006-2009, Red Hat, Inc and Others
 # SPDX-FileCopyrightText: Michael DeHaan <michael.dehaan AT gmail>
 
-import uuid
-from typing import TYPE_CHECKING, List, Optional, Union
+import copy
+from typing import TYPE_CHECKING, List, Union, Any
 
 from cobbler import autoinstall_manager
 from cobbler.items import item
 from cobbler import validate, enums
 from cobbler.utils import input_converters
 from cobbler.cexceptions import CX
-from cobbler.decorator import InheritableProperty
+from cobbler.decorator import LazyProperty, InheritableProperty
 
 if TYPE_CHECKING:
     from cobbler.api import CobblerAPI
@@ -28,14 +28,13 @@ class Profile(item.Item):
     TYPE_NAME = "profile"
     COLLECTION_TYPE = "profile"
 
-    def __init__(self, api: "CobblerAPI", *args, **kwargs):
+    def __init__(self, api: "CobblerAPI", *args: Any, **kwargs: Any):
         """
+        Constructor
 
         :param api: The Cobbler API object which is used for resolving information.
-        :param args:
-        :param kwargs:
         """
-        super().__init__(api, *args, **kwargs)
+        super().__init__(api)
         # Prevent attempts to clear the to_dict cache before the object is initialized.
         self._has_initialized = False
 
@@ -81,10 +80,13 @@ def __init__(self, api: "CobblerAPI", *args, **kwargs):
         # Use setters to validate settings
         self.virt_disk_driver = api.settings().default_virt_disk_driver
         self.virt_type = api.settings().default_virt_type
+
+        if len(kwargs) > 0:
+            self.from_dict(kwargs)
         if not self._has_initialized:
             self._has_initialized = True
 
-    def __getattr__(self, name):
+    def __getattr__(self, name: str) -> Any:
         if name == "kickstart":
             return self.autoinstall
         if name == "ks_meta":
@@ -103,11 +105,9 @@ def make_clone(self):
 
         :return: The cloned instance of this object.
         """
-        _dict = self.to_dict()
-        cloned = Profile(self.api)
-        cloned.from_dict(_dict)
-        cloned.uid = uuid.uuid4().hex
-        return cloned
+        _dict = copy.deepcopy(self.to_dict())
+        _dict.pop("uid", None)
+        return Profile(self.api, **_dict)
 
     def check_if_valid(self):
         """
@@ -117,27 +117,14 @@ def check_if_valid(self):
         """
         # name validation
         super().check_if_valid()
+        if not self.inmemory:
+            return
 
         # distro validation
         distro = self.get_conceptual_parent()
         if distro is None:
             raise CX(f"Error with profile {self.name} - distro is required")
 
-    def from_dict(self, dictionary: dict):
-        """
-        Initializes the object with attributes from the dictionary.
-
-        :param dictionary: The dictionary with values.
-        """
-        if "name" in dictionary:
-            self.name = dictionary["name"]
-        if "parent" in dictionary:
-            self.parent = dictionary["parent"]
-        if "distro" in dictionary:
-            self.distro = dictionary["distro"]
-        self._remove_depreacted_dict_keys(dictionary)
-        super().from_dict(dictionary)
-
     #
     # specific methods for item.Profile
     #
@@ -155,7 +142,7 @@ def arch(self):
             return parent.arch
         return None
 
-    @property
+    @LazyProperty
     def distro(self):
         """
         The parent distro of a profile. This is not representing the Distro but the id of it.
@@ -295,7 +282,7 @@ def enable_menu(self, enable_menu: bool):
             raise TypeError("enable_menu needs to be of type bool")
         self._enable_menu = enable_menu
 
-    @property
+    @LazyProperty
     def dhcp_tag(self) -> str:
         """
         Represents the VLAN tag the DHCP Server is in/answering to.
@@ -467,7 +454,7 @@ def virt_auto_boot(self, num: bool):
             return
         self._virt_auto_boot = validate.validate_virt_auto_boot(num)
 
-    @property
+    @LazyProperty
     def virt_cpus(self) -> int:
         """
         The amount of vCPU cores used in case the image is being deployed on top of a VM host.
@@ -594,7 +581,7 @@ def virt_bridge(self, vbridge: str):
         """
         self._virt_bridge = validate.validate_virt_bridge(vbridge)
 
-    @property
+    @LazyProperty
     def virt_path(self) -> str:
         """
         The path to the place where the image will be stored.
@@ -613,7 +600,7 @@ def virt_path(self, path: str):
         """
         self._virt_path = validate.validate_virt_path(path)
 
-    @property
+    @LazyProperty
     def repos(self) -> list:
         """
         The repositories to add once the system is provisioned.
@@ -704,7 +691,7 @@ def boot_loaders(self, boot_loaders: list):
         else:
             self._boot_loaders = []
 
-    @property
+    @LazyProperty
     def menu(self) -> str:
         r"""
         Property to represent the menu which this image should be put into.
@@ -730,7 +717,7 @@ def menu(self, menu: str):
                 raise CX(f"menu {menu} not found")
         self._menu = menu
 
-    @property
+    @LazyProperty
     def display_name(self) -> str:
         """
         Returns the display name.
diff --git a/cobbler/items/repo.py b/cobbler/items/repo.py
index 92a151f99d..c9874fee72 100644
--- a/cobbler/items/repo.py
+++ b/cobbler/items/repo.py
@@ -6,14 +6,17 @@
 # SPDX-FileCopyrightText: Copyright 2006-2009, Red Hat, Inc and Others
 # SPDX-FileCopyrightText: Michael DeHaan <michael.dehaan AT gmail>
 
-import uuid
-from typing import Union
+import copy
+from typing import TYPE_CHECKING, Any, Union
 
 from cobbler import enums
 from cobbler.utils import input_converters
 from cobbler.cexceptions import CX
 from cobbler.items import item
-from cobbler.decorator import InheritableProperty
+from cobbler.decorator import LazyProperty, InheritableProperty
+
+if TYPE_CHECKING:
+    from cobbler.api import CobblerAPI
 
 
 class Repo(item.Item):
@@ -24,14 +27,13 @@ class Repo(item.Item):
     TYPE_NAME = "repo"
     COLLECTION_TYPE = "repo"
 
-    def __init__(self, api, *args, **kwargs):
+    def __init__(self, api: "CobblerAPI", *args: Any, **kwargs: Any):
         """
+        Constructor
 
         :param api: The Cobbler API object which is used for resolving information.
-        :param args: The arguments which should be passed additionally to the base Item class constructor.
-        :param kwargs: The keyword arguments which should be passed additionally to the base Item class constructor.
         """
-        super().__init__(api, *args, **kwargs)
+        super().__init__(api)
         # Prevent attempts to clear the to_dict cache before the object is initialized.
         self._has_initialized = False
 
@@ -51,6 +53,9 @@ def __init__(self, api, *args, **kwargs):
         self._proxy = enums.VALUE_INHERITED
         self._rpm_list = []
         self._os_version = ""
+
+        if len(kwargs) > 0:
+            self.from_dict(kwargs)
         if not self._has_initialized:
             self._has_initialized = True
 
@@ -64,20 +69,9 @@ def make_clone(self):
 
         :return: The cloned instance of this object.
         """
-        _dict = self.to_dict()
-        cloned = Repo(self.api)
-        cloned.from_dict(_dict)
-        cloned.uid = uuid.uuid4().hex
-        return cloned
-
-    def from_dict(self, dictionary: dict):
-        """
-        Initializes the object with attributes from the dictionary.
-
-        :param dictionary: The dictionary with values.
-        """
-        self._remove_depreacted_dict_keys(dictionary)
-        super().from_dict(dictionary)
+        _dict = copy.deepcopy(self.to_dict())
+        _dict.pop("uid", None)
+        return Repo(self.api, **_dict)
 
     def check_if_valid(self):
         """
@@ -86,6 +80,8 @@ def check_if_valid(self):
         :raises CX: In case the name or mirror is missing.
         """
         super().check_if_valid()
+        if not self.inmemory:
+            return
         if self.mirror is None:
             raise CX(f"Error with repo {self.name} - mirror is required")
 
@@ -110,7 +106,7 @@ def _guess_breed(self):
             else:
                 self.breed = enums.RepoBreeds.RSYNC
 
-    @property
+    @LazyProperty
     def mirror(self) -> str:
         r"""
         A repo is (initially, as in right now) is something that can be rsynced. reposync/repotrack integration over
@@ -139,7 +135,7 @@ def mirror(self, mirror: str):
                 self.arch = enums.RepoArchs.I386
         self._guess_breed()
 
-    @property
+    @LazyProperty
     def mirror_type(self) -> enums.MirrorType:
         r"""
         Override the mirror_type used for reposync
@@ -172,7 +168,7 @@ def mirror_type(self, mirror_type: Union[str, enums.MirrorType]):
             raise TypeError("mirror_type needs to be of type enums.MirrorType")
         self._mirror_type = mirror_type
 
-    @property
+    @LazyProperty
     def keep_updated(self) -> bool:
         r"""
         This allows the user to disable updates to a particular repo for whatever reason.
@@ -197,7 +193,7 @@ def keep_updated(self, keep_updated: bool):
             )
         self._keep_updated = keep_updated
 
-    @property
+    @LazyProperty
     def yumopts(self) -> dict:
         r"""
         Options for the yum tool. Should be presented in the same way as the ``kernel_options``.
@@ -222,7 +218,7 @@ def yumopts(self, options: Union[str, dict]):
         except TypeError as error:
             raise TypeError("invalid yum options") from error
 
-    @property
+    @LazyProperty
     def rsyncopts(self) -> dict:
         r"""
         Options for ``rsync`` when being used for repo management.
@@ -247,7 +243,7 @@ def rsyncopts(self, options: Union[str, dict]):
         except TypeError as error:
             raise TypeError("invalid rsync options") from error
 
-    @property
+    @LazyProperty
     def environment(self) -> dict:
         """
         Yum can take options from the environment. This puts them there before each reposync.
@@ -272,7 +268,7 @@ def environment(self, options: Union[str, dict]):
         except TypeError as error:
             raise TypeError("invalid environment") from error
 
-    @property
+    @LazyProperty
     def priority(self) -> int:
         """
         Set the priority of the repository. Only works if host is using priorities plugin for yum.
@@ -301,7 +297,7 @@ def priority(self, priority: int):
             )
         self._priority = converted_value
 
-    @property
+    @LazyProperty
     def rpm_list(self) -> list:
         """
         Rather than mirroring the entire contents of a repository (Fedora Extras, for instance, contains games, and we
@@ -349,7 +345,7 @@ def createrepo_flags(self, createrepo_flags: str):
             )
         self._createrepo_flags = createrepo_flags
 
-    @property
+    @LazyProperty
     def breed(self) -> enums.RepoBreeds:
         """
         The repository system breed. This decides some defaults for most actions with a repo in Cobbler.
@@ -370,7 +366,7 @@ def breed(self, breed: Union[str, enums.RepoBreeds]):
         """
         self._breed = enums.RepoBreeds.to_enum(breed)
 
-    @property
+    @LazyProperty
     def os_version(self) -> str:
         r"""
         The operating system version which is compatible with this repository.
@@ -400,7 +396,7 @@ def os_version(self, os_version: str):
         self._os_version = os_version
         return
 
-    @property
+    @LazyProperty
     def arch(self) -> enums.RepoArchs:
         r"""
         Override the arch used for reposync
@@ -429,7 +425,7 @@ def arch(self, arch: Union[str, enums.RepoArchs]):
                 ) from error
         self._arch = enums.RepoArchs.to_enum(arch)
 
-    @property
+    @LazyProperty
     def mirror_locally(self) -> bool:
         r"""
         If this property is set to ``True`` then all content of the source is mirrored locally. This may take up a lot
@@ -453,7 +449,7 @@ def mirror_locally(self, value: bool):
             raise TypeError("mirror_locally needs to be of type bool")
         self._mirror_locally = value
 
-    @property
+    @LazyProperty
     def apt_components(self) -> list:
         """
         Specify the section of Debian to mirror. Defaults to "main,contrib,non-free,main/debian-installer".
@@ -472,7 +468,7 @@ def apt_components(self, value: Union[str, list]):
         """
         self._apt_components = input_converters.input_string_or_list(value)
 
-    @property
+    @LazyProperty
     def apt_dists(self) -> list:
         r"""
         This decides which installer images are downloaded. For more information please see:
diff --git a/cobbler/items/resource.py b/cobbler/items/resource.py
index 8555c7773e..4608abfc05 100644
--- a/cobbler/items/resource.py
+++ b/cobbler/items/resource.py
@@ -6,12 +6,15 @@
 # SPDX-FileCopyrightText: Copyright 2006-2009, Red Hat, Inc and Others
 # SPDX-FileCopyrightText: Kelsey Hightower <kelsey.hightower@gmail.com>
 
-import uuid
-from typing import Union
+import copy
+from typing import TYPE_CHECKING, Any, Union
 
 from cobbler import enums
-
 from cobbler.items import item
+from cobbler.decorator import LazyProperty
+
+if TYPE_CHECKING:
+    from cobbler.api import CobblerAPI
 
 
 class Resource(item.Item):
@@ -21,11 +24,13 @@ class Resource(item.Item):
     TODO: Type declarations in the method signatures and type checks in the bodys.
     """
 
-    def __init__(self, api, *args, **kwargs):
+    def __init__(self, api: "CobblerAPI", *args: Any, **kwargs: Any):
         """
         Constructor.
+
+        :param api: The Cobbler API object which is used for resolving information.
         """
-        super().__init__(api, *args, **kwargs)
+        super().__init__(api)
         # Prevent attempts to clear the to_dict cache before the object is initialized.
         self._has_initialized = False
 
@@ -35,6 +40,9 @@ def __init__(self, api, *args, **kwargs):
         self._group = ""
         self._path = ""
         self._template = ""
+
+        if len(kwargs) > 0:
+            self.from_dict(kwargs)
         if not self._has_initialized:
             self._has_initialized = True
 
@@ -48,26 +56,15 @@ def make_clone(self):
 
         :return: The cloned instance of this object.
         """
-        _dict = self.to_dict()
-        cloned = Resource(self.api)
-        cloned.from_dict(_dict)
-        cloned.uid = uuid.uuid4().hex
-        return cloned
-
-    def from_dict(self, dictionary: dict):
-        """
-        Initializes the object with attributes from the dictionary.
-
-        :param dictionary: The dictionary with values.
-        """
-        self._remove_depreacted_dict_keys(dictionary)
-        super().from_dict(dictionary)
+        _dict = copy.deepcopy(self.to_dict())
+        _dict.pop("uid", None)
+        return Resource(self.api, **_dict)
 
     #
     # specific methods for item.File
     #
 
-    @property
+    @LazyProperty
     def action(self) -> enums.ResourceAction:
         """
         Action property.
@@ -90,7 +87,7 @@ def action(self, action: Union[str, enums.ResourceAction]):
         """
         self._action = enums.ResourceAction.to_enum(action)
 
-    @property
+    @LazyProperty
     def group(self) -> str:
         """
         Group property.
@@ -112,7 +109,7 @@ def group(self, group: str):
             raise TypeError("Field group of object resource needs to be of type str!")
         self._group = group
 
-    @property
+    @LazyProperty
     def mode(self) -> str:
         """
         Mode property.
@@ -134,7 +131,7 @@ def mode(self, mode: str):
             raise TypeError("Field mode in object resource needs to be of type str!")
         self._mode = mode
 
-    @property
+    @LazyProperty
     def owner(self) -> str:
         """
         Owner property.
@@ -156,7 +153,7 @@ def owner(self, owner: str):
             raise TypeError("Field owner in object resource needs to be of type str!")
         self._owner = owner
 
-    @property
+    @LazyProperty
     def path(self) -> str:
         """
         Path property.
@@ -178,7 +175,7 @@ def path(self, path: str):
             raise TypeError("Field path in object resource needs to be of type str!")
         self._path = path
 
-    @property
+    @LazyProperty
     def template(self) -> str:
         """
         Template property.
diff --git a/cobbler/items/system.py b/cobbler/items/system.py
index 2b3f915623..a5801f48d9 100644
--- a/cobbler/items/system.py
+++ b/cobbler/items/system.py
@@ -7,8 +7,8 @@
 
 import enum
 import logging
-import uuid
-from typing import Any, Dict, List, Optional, Union
+import copy
+from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
 
 from ipaddress import AddressValueError
 
@@ -16,7 +16,10 @@
 from cobbler.utils import input_converters, filesystem_helpers
 from cobbler.cexceptions import CX
 from cobbler.items.item import Item
-from cobbler.decorator import InheritableProperty
+from cobbler.decorator import LazyProperty, InheritableProperty
+
+if TYPE_CHECKING:
+    from cobbler.api import CobblerAPI
 
 
 class NetworkInterface:
@@ -24,7 +27,7 @@ class NetworkInterface:
     A subobject of a Cobbler System which represents the network interfaces
     """
 
-    def __init__(self, api):
+    def __init__(self, api: "CobblerAPI", *args: Any, **kwargs: Any):
         """
         Constructor.
 
@@ -34,7 +37,7 @@ def __init__(self, api):
         self.__api = api
         self._bonding_opts = ""
         self._bridge_opts = ""
-        self._cnames = []
+        self._cnames: List[str] = []
         self._connected_mode = False
         self._dhcp_tag = ""
         self._dns_name = ""
@@ -46,17 +49,20 @@ def __init__(self, api):
         self._ipv6_default_gateway = ""
         self._ipv6_mtu = ""
         self._ipv6_prefix = ""
-        self._ipv6_secondaries = []
-        self._ipv6_static_routes = []
+        self._ipv6_secondaries: List[str] = []
+        self._ipv6_static_routes: List[str] = []
         self._mac_address = ""
         self._management = False
         self._mtu = ""
         self._netmask = ""
         self._static = False
-        self._static_routes = []
+        self._static_routes: List[str] = []
         self._virt_bridge = enums.VALUE_INHERITED
 
-    def from_dict(self, dictionary: dict):
+        if len(kwargs) > 0:
+            self.from_dict(kwargs)
+
+    def from_dict(self, dictionary: Dict[str, Any]):
         """
         Initializes the object with attributes from the dictionary.
 
@@ -74,7 +80,7 @@ def from_dict(self, dictionary: dict):
                 str(dictionary_keys),
             )
 
-    def to_dict(self, resolved: bool = False) -> dict:
+    def to_dict(self, resolved: bool = False) -> Dict[str, Any]:
         """
         This converts everything in this object to a dictionary.
 
@@ -82,7 +88,7 @@ def to_dict(self, resolved: bool = False) -> dict:
                          objects raw value.
         :return: A dictionary with all values present in this object.
         """
-        result = {}
+        result: Dict[str, Any] = {}
         for key, key_value in self.__dict__.items():
             if "__" in key:
                 continue
@@ -101,16 +107,16 @@ def to_dict(self, resolved: bool = False) -> dict:
         return result
 
     # These two methods are currently not used, but we do want to use them in the future, so let's define them.
-    def serialize(self):
+    def serialize(self) -> Dict[str, Any]:
         """
         This method is a proxy for :meth:`~cobbler.items.item.Item.to_dict` and contains additional logic for
         serialization to a persistent location.
 
         :return: The dictionary with the information for serialization.
         """
-        self.to_dict()
+        return self.to_dict()
 
-    def deserialize(self, interface_dict: dict):
+    def deserialize(self, interface_dict: Dict[str, Any]):
         """
         This is currently a proxy for :py:meth:`~cobbler.items.item.Item.from_dict` .
 
@@ -142,7 +148,7 @@ def dhcp_tag(self, dhcp_tag: str):
         self._dhcp_tag = dhcp_tag
 
     @property
-    def cnames(self) -> list:
+    def cnames(self) -> List[str]:
         """
         cnames property.
 
@@ -152,7 +158,7 @@ def cnames(self) -> list:
         return self._cnames
 
     @cnames.setter
-    def cnames(self, cnames: list):
+    def cnames(self, cnames: List[str]):
         """
         Setter for the cnames of the NetworkInterface class.
 
@@ -161,7 +167,7 @@ def cnames(self, cnames: list):
         self._cnames = input_converters.input_string_or_list_no_inherit(cnames)
 
     @property
-    def static_routes(self) -> list:
+    def static_routes(self) -> List[str]:
         """
         static_routes property.
 
@@ -171,7 +177,7 @@ def static_routes(self) -> list:
         return self._static_routes
 
     @static_routes.setter
-    def static_routes(self, routes: list):
+    def static_routes(self, routes: List[str]):
         """
         Setter for the static_routes of the NetworkInterface class.
 
@@ -695,7 +701,7 @@ def connected_mode(self, truthiness: bool):
             ) from error
         self._connected_mode = truthiness
 
-    def modify_interface(self, _dict: dict):
+    def modify_interface(self, _dict: Dict[str, Any]):
         """
         Modify the interface
 
@@ -762,13 +768,13 @@ class System(Item):
     TYPE_NAME = "system"
     COLLECTION_TYPE = "system"
 
-    def __init__(self, api, *args, **kwargs):
+    def __init__(self, api: "CobblerAPI", *args: Any, **kwargs: Any):
         """
         Constructor
 
         :param api: The Cobbler API
         """
-        super().__init__(api, *args, **kwargs)
+        super().__init__(api)
         # Prevent attempts to clear the to_dict cache before the object is initialized.
         self._has_initialized = False
 
@@ -776,7 +782,7 @@ def __init__(self, api, *args, **kwargs):
         self._ipv6_autoconfiguration = False
         self._repos_enabled = False
         self._autoinstall = enums.VALUE_INHERITED
-        self._boot_loaders: Union[list, str] = enums.VALUE_INHERITED
+        self._boot_loaders: Union[List[str], str] = enums.VALUE_INHERITED
         self._enable_ipxe: Union[bool, str] = enums.VALUE_INHERITED
         self._gateway = ""
         self._hostname = ""
@@ -821,10 +827,13 @@ def __init__(self, api, *args, **kwargs):
         self._kernel_options_post = enums.VALUE_INHERITED
         self._mgmt_parameters = enums.VALUE_INHERITED
         self._mgmt_classes = enums.VALUE_INHERITED
+
+        if len(kwargs) > 0:
+            self.from_dict(kwargs)
         if not self._has_initialized:
             self._has_initialized = True
 
-    def __getattr__(self, name):
+    def __getattr__(self, name: str) -> Any:
         if name == "kickstart":
             return self.autoinstall
         if name == "ks_meta":
@@ -836,28 +845,9 @@ def __getattr__(self, name):
     #
 
     def make_clone(self):
-        _dict = self.to_dict()
-        cloned = System(self.api)
-        cloned.from_dict(_dict)
-        cloned.uid = uuid.uuid4().hex
-        return cloned
-
-    def from_dict(self, dictionary: dict):
-        """
-        Initializes the object with attributes from the dictionary.
-
-        :param dictionary: The dictionary with values.
-        """
-        if "name" in dictionary:
-            self.name = dictionary["name"]
-        if "parent" in dictionary:
-            self.parent = dictionary["parent"]
-        if "profile" in dictionary:
-            self.profile = dictionary["profile"]
-        if "image" in dictionary:
-            self.image = dictionary["image"]
-        self._remove_depreacted_dict_keys(dictionary)
-        super().from_dict(dictionary)
+        _dict = copy.deepcopy(self.to_dict())
+        _dict.pop("uid", None)
+        return System(self.api, **_dict)
 
     def check_if_valid(self):
         """
@@ -866,6 +856,8 @@ def check_if_valid(self):
         :raises CX: In case name is missing. Additionally either image or profile is required.
         """
         super().check_if_valid()
+        if not self.inmemory:
+            return
 
         # System specific validation
         if self.profile is None or self.profile == "":
@@ -878,7 +870,7 @@ def check_if_valid(self):
     # specific methods for item.System
     #
 
-    @property
+    @LazyProperty
     def interfaces(self) -> Dict[str, NetworkInterface]:
         r"""
         Represents all interfaces owned by the system.
@@ -959,7 +951,7 @@ def rename_interface(self, old_name: str, new_name: str):
         self.interfaces[new_name] = self.interfaces[old_name]
         del self.interfaces[old_name]
 
-    @property
+    @LazyProperty
     def hostname(self) -> str:
         """
         hostname property.
@@ -980,7 +972,7 @@ def hostname(self, value: str):
             raise TypeError("Field hostname of object system needs to be of type str!")
         self._hostname = value
 
-    @property
+    @LazyProperty
     def status(self) -> str:
         """
         status property.
@@ -1281,7 +1273,7 @@ def __get_interface(self, interface_name: str = "default") -> NetworkInterface:
             self.__create_interface(interface_name)
         return self._interfaces[interface_name]
 
-    @property
+    @LazyProperty
     def gateway(self):
         """
         gateway property.
@@ -1301,7 +1293,7 @@ def gateway(self, gateway: str):
         """
         self._gateway = validate.ipv4_address(gateway)
 
-    @property
+    @LazyProperty
     def name_servers(self) -> list:
         """
         name_servers property.
@@ -1323,7 +1315,7 @@ def name_servers(self, data: Union[str, list]):
         """
         self._name_servers = validate.name_servers(data)
 
-    @property
+    @LazyProperty
     def name_servers_search(self) -> list:
         """
         name_servers_search property.
@@ -1343,7 +1335,7 @@ def name_servers_search(self, data: Union[str, list]):
         """
         self._name_servers_search = validate.name_servers_search(data)
 
-    @property
+    @LazyProperty
     def ipv6_autoconfiguration(self) -> bool:
         """
         ipv6_autoconfiguration property.
@@ -1365,7 +1357,7 @@ def ipv6_autoconfiguration(self, value: bool):
             raise TypeError("ipv6_autoconfiguration needs to be of type bool")
         self._ipv6_autoconfiguration = value
 
-    @property
+    @LazyProperty
     def ipv6_default_device(self) -> str:
         """
         ipv6_default_device property.
@@ -1415,7 +1407,7 @@ def enable_ipxe(self, enable_ipxe: bool):
             raise TypeError("enable_ipxe needs to be of type bool")
         self._enable_ipxe = enable_ipxe
 
-    @property
+    @LazyProperty
     def profile(self) -> str:
         """
         profile property.
@@ -1450,7 +1442,7 @@ def profile(self, profile_name: str):
         self._profile = profile_name
         self.depth = profile.depth + 1  # subprofiles have varying depths.
 
-    @property
+    @LazyProperty
     def image(self) -> str:
         """
         image property.
@@ -1573,7 +1565,7 @@ def virt_auto_boot(self, value: bool):
             return
         self._virt_auto_boot = validate.validate_virt_auto_boot(value)
 
-    @property
+    @LazyProperty
     def virt_pxe_boot(self) -> bool:
         """
         virt_pxe_boot property.
@@ -1656,7 +1648,7 @@ def virt_path(self, path: str):
         """
         self._virt_path = validate.validate_virt_path(path, for_system=True)
 
-    @property
+    @LazyProperty
     def netboot_enabled(self) -> bool:
         """
         netboot_enabled property.
@@ -1710,7 +1702,7 @@ def autoinstall(self, autoinstall: str):
             autoinstall
         )
 
-    @property
+    @LazyProperty
     def power_type(self) -> str:
         """
         power_type property.
@@ -1736,7 +1728,7 @@ def power_type(self, power_type: str):
         power_manager.validate_power_type(power_type)
         self._power_type = power_type
 
-    @property
+    @LazyProperty
     def power_identity_file(self) -> str:
         """
         power_identity_file property.
@@ -1761,7 +1753,7 @@ def power_identity_file(self, power_identity_file: str):
         filesystem_helpers.safe_filter(power_identity_file)
         self._power_identity_file = power_identity_file
 
-    @property
+    @LazyProperty
     def power_options(self) -> str:
         """
         power_options property.
@@ -1786,7 +1778,7 @@ def power_options(self, power_options: str):
         filesystem_helpers.safe_filter(power_options)
         self._power_options = power_options
 
-    @property
+    @LazyProperty
     def power_user(self) -> str:
         """
         power_user property.
@@ -1811,7 +1803,7 @@ def power_user(self, power_user: str):
         filesystem_helpers.safe_filter(power_user)
         self._power_user = power_user
 
-    @property
+    @LazyProperty
     def power_pass(self) -> str:
         """
         power_pass property.
@@ -1836,7 +1828,7 @@ def power_pass(self, power_pass: str):
         filesystem_helpers.safe_filter(power_pass)
         self._power_pass = power_pass
 
-    @property
+    @LazyProperty
     def power_address(self) -> str:
         """
         power_address property.
@@ -1861,7 +1853,7 @@ def power_address(self, power_address: str):
         filesystem_helpers.safe_filter(power_address)
         self._power_address = power_address
 
-    @property
+    @LazyProperty
     def power_id(self) -> str:
         """
         power_id property.
@@ -1884,7 +1876,7 @@ def power_id(self, power_id: str):
         filesystem_helpers.safe_filter(power_id)
         self._power_id = power_id
 
-    @property
+    @LazyProperty
     def repos_enabled(self) -> bool:
         """
         repos_enabled property.
@@ -1909,7 +1901,7 @@ def repos_enabled(self, repos_enabled: bool):
             )
         self._repos_enabled = repos_enabled
 
-    @property
+    @LazyProperty
     def serial_device(self) -> int:
         """
         serial_device property. "-1" disables the serial device functionality completely.
@@ -1928,7 +1920,7 @@ def serial_device(self, device_number: int):
         """
         self._serial_device = validate.validate_serial_device(device_number)
 
-    @property
+    @LazyProperty
     def serial_baud_rate(self) -> enums.BaudRates:
         """
         serial_baud_rate property. The value "disabled" will disable the functionality completely.
@@ -1988,7 +1980,7 @@ def get_config_filename(self, interface: str, loader: Optional[str] = None):
             return utils.get_host_ip(ip_address)
         return self.name
 
-    @property
+    @LazyProperty
     def display_name(self) -> str:
         """
         Returns the display name.
diff --git a/cobbler/modules/serializers/__init__.py b/cobbler/modules/serializers/__init__.py
index a06053dd7c..ee3b88d6af 100644
--- a/cobbler/modules/serializers/__init__.py
+++ b/cobbler/modules/serializers/__init__.py
@@ -66,6 +66,19 @@ def deserialize(self, collection, topological: bool = True):
             "The implementation for the configured serializer is missing!"
         )
 
+    def deserialize_item(self, collection_type: str, name: str) -> dict:
+        """
+        Get a collection item from database and parse it into an object.
+
+        :param collection_type: The collection type to fetch.
+        :param item: collection item
+        :param topological: If the collection list should be sorted by the collection dict depth value or not.
+        :return: The first element of the collection requested.
+        """
+        raise NotImplementedError(
+            "The implementation for the configured serializer is missing!"
+        )
+
 
 def register() -> str:
     """
diff --git a/cobbler/modules/serializers/file.py b/cobbler/modules/serializers/file.py
index 050706032c..136a4f6a24 100644
--- a/cobbler/modules/serializers/file.py
+++ b/cobbler/modules/serializers/file.py
@@ -12,6 +12,7 @@
 import glob
 import json
 import logging
+from typing import Any, Dict
 
 import cobbler.api as capi
 from cobbler import settings
@@ -101,12 +102,21 @@ def deserialize_raw(self, collection_type: str):
 
         path = os.path.join(self.libpath, collection_type)
         all_files = glob.glob(f"{path}/*.json")
+        lazy_start = self.api.settings().lazy_start
 
         for file in all_files:
-            with open(file, encoding="UTF-8") as file_descriptor:
-                json_data = file_descriptor.read()
-                _dict = json.loads(json_data)
-                results.append(_dict)
+            (name, _) = os.path.splitext(os.path.basename(file))
+            if lazy_start:
+                _dict = {"name": name, "inmemory": False}
+            else:
+                with open(file, encoding="UTF-8") as file_descriptor:
+                    json_data = file_descriptor.read()
+                    _dict = json.loads(json_data)
+                    if _dict["name"] != name:
+                        raise CX(
+                            f"The file name {name}.json does not match the {_dict['name']} {collection_type}!"
+                        )
+            results.append(_dict)
         return results
 
     def deserialize(self, collection, topological: bool = True):
@@ -120,9 +130,30 @@ def deserialize(self, collection, topological: bool = True):
                 collection.from_list(datastruct)
         except Exception as exc:
             logger.error(
-                f"Error while loading a collection: {exc}. Skipping this collection!"
+                "Error while loading a collection: %s. Skipping collection %s!",
+                exc,
+                collection.collection_type(),
             )
 
+    def deserialize_item(self, collection_type: str, name: str) -> Dict[str, Any]:
+        """
+        Get a collection item from disk and parse it into an object.
+
+        :param collection_type: The collection type to fetch.
+        :param name: collection Item name
+        :return: Dictionary of the collection item.
+        """
+        path = os.path.join(self.libpath, collection_type, f"{name}.json")
+        with open(path, encoding="UTF-8") as file_descriptor:
+            json_data = file_descriptor.read()
+            _dict = json.loads(json_data)
+            if _dict["name"] != name:
+                raise CX(
+                    f"The file name {name}.json does not match the {_dict['name']} {collection_type}!"
+                )
+        _dict["inmemory"] = True
+        return _dict
+
 
 def storage_factory(api):
     """
diff --git a/cobbler/modules/serializers/mongodb.py b/cobbler/modules/serializers/mongodb.py
index be308e9831..1b1f08fbf2 100644
--- a/cobbler/modules/serializers/mongodb.py
+++ b/cobbler/modules/serializers/mongodb.py
@@ -101,18 +101,53 @@ def deserialize_raw(self, collection_type: str):
         if collection_type == "settings":
             return settings.read_settings_file()
 
+        results = []
+        projection = None
         collection = self.mongodb_database[collection_type]
-        return list(collection.find({}, {"_id": False}))
+        lazy_start = self.api.settings().lazy_start
+        if lazy_start:
+            projection = ["name"]
+
+        # pymongo.cursor.Cursor
+        cursor = collection.find(projection=projection)
+        for result in cursor:
+            self._remove_id(result)
+            result["inmemory"] = not lazy_start
+            results.append(result)
+        return results
 
     def deserialize(self, collection, topological: bool = True):
         datastruct = self.deserialize_raw(collection.collection_type())
         if topological and isinstance(datastruct, list):
-            datastruct.sort(key=lambda x: x["depth"])
+            datastruct.sort(key=lambda x: x.get("depth", 1))
         if isinstance(datastruct, dict):
             collection.from_dict(datastruct)
         elif isinstance(datastruct, list):
             collection.from_list(datastruct)
 
+    def deserialize_item(self, collection_type: str, name: str) -> dict:
+        """
+        Get a collection item from database.
+
+        :param collection_type: The collection type to fetch.
+        :param name: collection Item name
+        :return: Dictionary of the collection item.
+        """
+        mongodb_collection = self.mongodb_database[collection_type]
+        result = mongodb_collection.find_one({"name": name})
+        if result is None:
+            raise CX(
+                f"Item {name} of collection {collection_type} was not found in MongoDB database {self.database_name}!"
+            )
+        self._remove_id(result)
+        result["inmemory"] = True
+        return result
+
+    @staticmethod
+    def _remove_id(_dict: dict):
+        if "_id" in _dict:
+            _dict.pop("_id")
+
 
 def storage_factory(api):
     """
diff --git a/cobbler/remote.py b/cobbler/remote.py
index 1b8743a1c6..1eb541d742 100644
--- a/cobbler/remote.py
+++ b/cobbler/remote.py
@@ -18,7 +18,7 @@
 import re
 import xmlrpc.server
 from socketserver import ThreadingMixIn
-from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
+from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Union
 from xmlrpc.server import SimpleXMLRPCRequestHandler
 
 from cobbler import enums
@@ -63,7 +63,7 @@ def __init__(self, api: "CobblerAPI"):
         self.api = api
         self.logger = logging.getLogger()
         self.token_cache: Dict[str, tuple] = {}
-        self.unsaved_items = {}
+        self.unsaved_items: Dict[str, Any] = {}
         self.timestamp = self.api.last_modified_time()
         self.events: Dict[str, CobblerEvent] = {}
         self.shared_secret = utils.get_shared_secret()
@@ -427,12 +427,12 @@ def _new_event(self, name: str) -> CobblerEvent:
 
     def __start_task(
         self,
-        thr_obj_fn,
+        thr_obj_fn: Callable[[CobblerThread], None],
         token: str,
         role_name: str,
         name: str,
-        args: dict,
-        on_done=None,
+        args: Dict[str, Any],
+        on_done: Optional[Callable[[CobblerThread], None]] = None,
     ):
         """
         Starts a new background task.
@@ -1761,7 +1761,9 @@ def rename_menu(self, object_id: str, newname: str, token: Optional[str] = None)
         """
         return self.rename_item("menu", object_id, newname, token)
 
-    def new_item(self, what: str, token: str, is_subobject: bool = False):
+    def new_item(
+        self, what: str, token: str, is_subobject: bool = False, **kwargs: Any
+    ) -> str:
         """Creates a new (unconfigured) object, returning an object handle that can be used.
 
         Creates a new (unconfigured) object, returning an object handle that can be used with ``modify_*`` methods and
@@ -1775,7 +1777,7 @@ def new_item(self, what: str, token: str, is_subobject: bool = False):
         """
         self._log(f"new_item({what})", token=token)
         self.check_access(token, f"new_{what}")
-        new_item = self.api.new_item(what, is_subobject)
+        new_item = self.api.new_item(what, is_subobject, **kwargs)
         self.unsaved_items[new_item.uid] = (time.time(), new_item)
         return new_item.uid
 
diff --git a/cobbler/serializer.py b/cobbler/serializer.py
index 4c9267a770..4edea88ac0 100644
--- a/cobbler/serializer.py
+++ b/cobbler/serializer.py
@@ -12,6 +12,12 @@
 import pathlib
 import sys
 import time
+from typing import TYPE_CHECKING, Any, Dict
+
+if TYPE_CHECKING:
+    from cobbler.api import CobblerAPI
+    from cobbler.cobbler_collections.collection import Collection
+    from cobbler.items.item import Item
 
 
 class Serializer:
@@ -19,7 +25,7 @@ class Serializer:
     Serializer interface that is used to access data in Cobbler independent of the actual data source.
     """
 
-    def __init__(self, api):
+    def __init__(self, api: "CobblerAPI"):
         """
         Constructor that created the state for the object.
 
@@ -50,7 +56,7 @@ def __grab_lock(self):
             self.logger.exception("File locking error.", exc_info=exception)
             sys.exit(7)
 
-    def __release_lock(self, with_changes=False):
+    def __release_lock(self, with_changes: bool = False):
         """
         Releases the lock on the resource that is currently being written.
 
@@ -67,7 +73,7 @@ def __release_lock(self, with_changes=False):
             fcntl.flock(self.lock_handle.fileno(), fcntl.LOCK_UN)
             self.lock_handle.close()
 
-    def serialize(self, collection):
+    def serialize(self, collection: "Collection"):
         """
         Save a collection to disk
 
@@ -78,7 +84,7 @@ def serialize(self, collection):
         self.storage_object.serialize(collection)
         self.__release_lock()
 
-    def serialize_item(self, collection, item):
+    def serialize_item(self, collection: "Collection", item: "Item"):
         """
         Save a collection item to disk
 
@@ -90,7 +96,7 @@ def serialize_item(self, collection, item):
         self.storage_object.serialize_item(collection, item)
         self.__release_lock(with_changes=True)
 
-    def serialize_delete(self, collection, item):
+    def serialize_delete(self, collection: "Collection", item: "Item"):
         """
         Delete a collection item from disk
 
@@ -102,7 +108,7 @@ def serialize_delete(self, collection, item):
         self.storage_object.serialize_delete(collection, item)
         self.__release_lock(with_changes=True)
 
-    def deserialize(self, collection, topological: bool = True):
+    def deserialize(self, collection: "Collection", topological: bool = True):
         """
         Load a collection from disk.
 
@@ -115,6 +121,18 @@ def deserialize(self, collection, topological: bool = True):
         self.storage_object.deserialize(collection, topological)
         self.__release_lock()
 
+    def deserialize_item(self, collection_type: str, item_name: str) -> Dict[str, Any]:
+        """
+        Load a collection item from disk.
+
+        :param collection_type: The collection type to deserialize.
+        :param item_name: The collection item name to deserialize.
+        """
+        self.__grab_lock()
+        result = self.storage_object.deserialize_item(collection_type, item_name)
+        self.__release_lock()
+        return result
+
     def __get_storage_module(self):
         """
         Look up configured module in the settings
@@ -123,6 +141,8 @@ def __get_storage_module(self):
         """
         return self.api.get_module_from_file(
             "serializers",
-            self.api.settings().modules.get("serializers", {}).get("module"),
+            self.api.settings()
+            .modules.get("serializers", {})
+            .get("module", "serializers.file"),
             "serializers.file",
         )
diff --git a/cobbler/settings/__init__.py b/cobbler/settings/__init__.py
index 48144a3162..ae90f6e841 100644
--- a/cobbler/settings/__init__.py
+++ b/cobbler/settings/__init__.py
@@ -283,6 +283,7 @@ def __init__(self):
         self.windows_template_dir = "/etc/cobbler/windows"
         self.samba_distro_share = "DISTRO"
         self.cache_enabled = False
+        self.lazy_start = False
 
     def to_string(self) -> str:
         """
diff --git a/cobbler/settings/migrations/V3_4_0.py b/cobbler/settings/migrations/V3_4_0.py
index a8c4dda85a..f20552f95b 100644
--- a/cobbler/settings/migrations/V3_4_0.py
+++ b/cobbler/settings/migrations/V3_4_0.py
@@ -168,6 +168,7 @@
         },
         Optional("cache_enabled"): bool,
         Optional("autoinstall_scheme"): str,
+        Optional("lazy_start"): bool,
     },
     ignore_extra_keys=False,
 )
diff --git a/cobbler/utils/thread.py b/cobbler/utils/thread.py
index 8c6080c3d6..a7e0616244 100644
--- a/cobbler/utils/thread.py
+++ b/cobbler/utils/thread.py
@@ -5,11 +5,15 @@
 import logging
 import pathlib
 from threading import Thread
-from typing import Optional, Callable
+from typing import TYPE_CHECKING, Any, Dict, List, Optional, Callable, Union
 
 from cobbler import enums
 from cobbler import utils
 
+if TYPE_CHECKING:
+    from cobbler.api import CobblerAPI
+    from cobbler.remote import CobblerXMLRPCInterface
+
 
 class CobblerThread(Thread):
     """
@@ -19,12 +23,12 @@ class CobblerThread(Thread):
     def __init__(
         self,
         event_id: str,
-        remote,
-        options: dict,
+        remote: "CobblerXMLRPCInterface",
+        options: Optional[Union[List[str], Dict[str, Any]]],
         task_name: str,
-        api,
-        run: Callable,
-        on_done: Optional[Callable] = None,
+        api: "CobblerAPI",
+        run: Callable[["CobblerThread"], None],
+        on_done: Optional[Callable[["CobblerThread"], None]] = None,
     ):
         """
         This constructor creates a Cobbler thread which then may be run by calling ``run()``.
@@ -41,12 +45,12 @@ def __init__(
         self.event_id = event_id
         self.remote = remote
         self.logger = logging.getLogger()
-        self.__task_log_handler = None
+        self.__task_log_handler: Optional[logging.FileHandler] = None
         self.__setup_logger()
         self._run = run
         self.on_done = on_done
         if options is None:
-            options = {}
+            options = []
         self.options = options
         self.task_name = task_name
         self.api = api
@@ -70,7 +74,7 @@ def _set_task_state(self, new_state: enums.EventStatus):
 
         :param new_state: The new state of the task.
         """
-        if not isinstance(new_state, enums.EventStatus):
+        if not isinstance(new_state, enums.EventStatus):  # type: ignore
             raise TypeError('"new_state" needs to be of type enums.EventStatus!')
         if self.event_id not in self.remote.events:
             raise ValueError('"event_id" not existing!')
@@ -82,7 +86,7 @@ def _set_task_state(self, new_state: enums.EventStatus):
         elif new_state == enums.EventStatus.FAILED:
             self.logger.error("### TASK FAILED ###")
 
-    def run(self):
+    def run(self) -> None:
         """
         Run the thread.
 
@@ -93,26 +97,27 @@ def run(self):
             if utils.run_triggers(
                 api=self.api,
                 globber=f"/var/lib/cobbler/triggers/task/{self.task_name}/pre/*",
-                additional=self.options,
+                additional=self.options if isinstance(self.options, list) else [],
             ):
                 self._set_task_state(enums.EventStatus.FAILED)
-                return False
+                return
             return_code = self._run(self)
             if return_code is not None and not return_code:
                 self._set_task_state(enums.EventStatus.FAILED)
             else:
                 self._set_task_state(enums.EventStatus.COMPLETE)
                 if self.on_done is not None:
-                    self.on_done()
+                    self.on_done(self)
                 utils.run_triggers(
                     api=self.api,
                     globber=f"/var/lib/cobbler/triggers/task/{self.task_name}/post/*",
-                    additional=self.options,
+                    additional=self.options if isinstance(self.options, list) else [],
                 )
             return return_code
         except Exception:
             utils.log_exc()
             self._set_task_state(enums.EventStatus.FAILED)
-            return False
+            return
         finally:
-            self.logger.removeHandler(self.__task_log_handler)
+            if self.__task_log_handler is not None:
+                self.logger.removeHandler(self.__task_log_handler)
diff --git a/config/cobbler/settings.yaml b/config/cobbler/settings.yaml
index c75b8b5b4d..0136f6a90e 100644
--- a/config/cobbler/settings.yaml
+++ b/config/cobbler/settings.yaml
@@ -627,3 +627,12 @@ cache_enabled: false
 # This is setting does not setup your api for https it just changes the way the url for your profiles and systems are
 # generated.
 autoinstall_scheme: "http"
+
+# Set to true to speed up the start of the Cobbler. When storing collections as files, the directory with the names
+# of the collection elements will be scanned without reading and parsing the files themselves. In the case of storing
+# collections in the database, a projection query is made that includes only the names of the collection elements.
+# The first time an attribute of an element other than a name is accessed, a full read of all other attributes will be
+# performed, and a recursive full read of all elements on which this element depends. At startup, a background task is
+# also launched, which, when idle, fills in all the properties of the elements of the collections.
+# Suitable for configurations with a large number of elements placed on a slow device (HDD, network).
+lazy_start: false
diff --git a/docs/cobbler-conf/settings-yaml.rst b/docs/cobbler-conf/settings-yaml.rst
index 311eae3467..a1d3c3a3de 100644
--- a/docs/cobbler-conf/settings-yaml.rst
+++ b/docs/cobbler-conf/settings-yaml.rst
@@ -1135,3 +1135,16 @@ cache_enabled
 If set to ``True``, allows the results of some internal operations to be cached, but may slow down editing of objects.
 
 default: ``False``
+
+lazy_start
+##########
+
+Set to ``True`` to speed up the start of the Cobbler. When storing collections as files, the directory with the names
+of the collection elements will be scanned without reading and parsing the files themselves. In the case of storing
+collections in the database, a projection query is made that includes only the names of the collection elements.
+The first time an attribute of an element other than a name is accessed, a full read of all other attributes will be
+performed, and a recursive full read of all elements on which this element depends. At startup, a background task is
+also launched, which, when idle, fills in all the properties of the elements of the collections.
+Suitable for configurations with a large number of elements placed on a slow device (HDD, network).
+
+default: ``False``


Test patch:
diff --git a/tests/items/inmemory_test.py b/tests/items/inmemory_test.py
new file mode 100644
index 0000000000..c3e42fb038
--- /dev/null
+++ b/tests/items/inmemory_test.py
@@ -0,0 +1,252 @@
+"""
+TODO
+"""
+
+from typing import Callable
+import pytest
+
+import os
+import pathlib
+
+from cobbler.api import CobblerAPI
+from cobbler.items.package import Package
+from cobbler.items.file import File
+from cobbler.items.mgmtclass import Mgmtclass
+from cobbler.items.repo import Repo
+from cobbler.items.image import Image
+from cobbler.items.distro import Distro
+from cobbler.items.menu import Menu
+from cobbler.items.profile import Profile
+from cobbler.items.system import System
+from cobbler.cobbler_collections import manager
+
+
+@pytest.fixture(scope="function")
+def inmemory_api() -> CobblerAPI:
+    """
+    TODO
+    """
+    CobblerAPI.__shared_state = {}
+    CobblerAPI.__has_loaded = False
+    api = CobblerAPI()
+    api.settings().lazy_start = True
+    manager.CollectionManager.has_loaded = False
+    manager.CollectionManager.__shared_state = {}
+    api._collection_mgr = manager.CollectionManager(api)
+    return api
+
+
+def test_inmemory(
+    inmemory_api: CobblerAPI,
+    create_kernel_initrd: Callable[[str, str], str],
+    fk_initrd: str,
+    fk_kernel: str,
+):
+    """
+    TODO
+    """
+    # Arrange
+    test_package = Package(
+        inmemory_api, **{"name": "test_package", "comment": "test comment"}
+    )
+    inmemory_api.add_package(test_package)
+    test_file = File(
+        inmemory_api,
+        **{
+            "name": "test_file",
+            "path": "test path",
+            "owner": "test owner",
+            "group": "test group",
+            "mode": "test mode",
+            "is_dir": True,
+            "comment": "test comment",
+        },
+    )
+    inmemory_api.add_file(test_file)
+    test_mgmtclass = Mgmtclass(
+        inmemory_api,
+        **{
+            "name": "test_mgmtclass",
+            "packages": [test_package.name],
+            "files": [test_file.name],
+            "comment": "test comment",
+        },
+    )
+    inmemory_api.add_mgmtclass(test_mgmtclass)
+    test_repo = Repo(inmemory_api, **{"name": "test_repo", "comment": "test comment"})
+    inmemory_api.add_repo(test_repo)
+    test_menu1 = Menu(inmemory_api, **{"name": "test_menu1", "comment": "test comment"})
+    inmemory_api.add_menu(test_menu1)
+    test_menu2 = Menu(
+        inmemory_api,
+        **{
+            "name": "test_menu2",
+            "parent": test_menu1.name,
+            "comment": "test comment",
+        },
+    )
+    inmemory_api.add_menu(test_menu2)
+
+    directory = create_kernel_initrd(fk_kernel, fk_initrd)
+    (pathlib.Path(directory) / "images").mkdir()
+    test_distro = Distro(
+        inmemory_api,
+        **{
+            "name": "test_distro",
+            "kernel": str(os.path.join(directory, fk_kernel)),
+            "initrd": str(os.path.join(directory, fk_initrd)),
+            "mgmt_classes": test_mgmtclass.name,
+            "comment": "test comment",
+        },
+    )
+    inmemory_api.add_distro(test_distro)
+
+    test_profile1 = Profile(
+        inmemory_api,
+        **{
+            "name": "test_profile1",
+            "distro": test_distro.name,
+            "enable_menu": False,
+            "repos": [test_repo.name],
+            "menu": test_menu1.name,
+            "comment": "test comment",
+        },
+    )
+    inmemory_api.add_profile(test_profile1)
+    test_profile2 = Profile(
+        inmemory_api,
+        **{
+            "name": "test_profile2",
+            "parent": test_profile1.name,
+            "enable_menu": False,
+            "menu": test_menu2.name,
+            "comment": "test comment",
+        },
+    )
+    inmemory_api.add_profile(test_profile2)
+    test_profile3 = Profile(
+        inmemory_api,
+        **{
+            "name": "test_profile3",
+            "parent": test_profile1.name,
+            "enable_menu": False,
+            "mgmt_classes": test_mgmtclass.name,
+            "repos": [test_repo.name],
+            "menu": test_menu1.name,
+            "comment": "test comment",
+        },
+    )
+    inmemory_api.add_profile(test_profile3)
+    test_image = Image(
+        inmemory_api,
+        **{"name": "test_image", "menu": test_menu1.name, "comment": "test comment"},
+    )
+    inmemory_api.add_image(test_image)
+    test_system1 = System(
+        inmemory_api,
+        **{
+            "name": "test_system1",
+            "profile": test_profile1.name,
+            "comment": "test comment",
+        },
+    )
+    inmemory_api.add_system(test_system1)
+    test_system2 = System(
+        inmemory_api,
+        **{"name": "test_system2", "image": test_image.name, "comment": "test comment"},
+    )
+    inmemory_api.add_system(test_system2)
+
+    inmemory_api.systems().listing.pop(test_system2.name)
+    inmemory_api.systems().listing.pop(test_system1.name)
+    inmemory_api.images().listing.pop(test_image.name)
+    inmemory_api.profiles().listing.pop(test_profile3.name)
+    inmemory_api.profiles().listing.pop(test_profile2.name)
+    inmemory_api.profiles().listing.pop(test_profile1.name)
+    inmemory_api.distros().listing.pop(test_distro.name)
+    inmemory_api.menus().listing.pop(test_menu2.name)
+    inmemory_api.menus().listing.pop(test_menu1.name)
+    inmemory_api.repos().listing.pop(test_repo.name)
+    inmemory_api.mgmtclasses().listing.pop(test_mgmtclass.name)
+    inmemory_api.files().listing.pop(test_file.name)
+    inmemory_api.packages().listing.pop(test_package.name)
+
+    inmemory_api.deserialize()
+
+    test_package = inmemory_api.find_package("test_package")
+    test_file = inmemory_api.find_file("test_file")
+    test_mgmtclass = inmemory_api.find_mgmtclass("test_mgmtclass")
+    test_repo = inmemory_api.find_repo("test_repo")
+    test_menu1 = inmemory_api.find_menu("test_menu1")
+    test_menu2 = inmemory_api.find_menu("test_menu2")
+    test_distro = inmemory_api.find_distro("test_distro")
+    test_profile1 = inmemory_api.find_profile("test_profile1")
+    test_profile2 = inmemory_api.find_profile("test_profile2")
+    test_profile3 = inmemory_api.find_profile("test_profile3")
+    test_image = inmemory_api.find_image("test_image")
+    test_system1 = inmemory_api.find_system("test_system1")
+    test_system2 = inmemory_api.find_system("test_system2")
+
+    # Act
+    result = True
+    for collection in [
+        "package",
+        "file",
+        "mgmtclass",
+        "repo",
+        "distro",
+        "menu",
+        "image",
+        "profile",
+        "system",
+    ]:
+        for obj in inmemory_api.get_items(collection):
+            result = obj.inmemory is False if result else result
+            result = obj.__dict__["_comment"] == "" if result else result
+
+    comment = test_system1.comment if result else result
+    result = test_package.inmemory if result else result
+    result = test_file.inmemory if result else result
+    result = test_mgmtclass.inmemory if result else result
+    result = test_repo.inmemory if result else result
+    result = test_menu1.inmemory if result else result
+    result = not test_menu2.inmemory if result else result
+    result = test_distro.inmemory if result else result
+    result = test_profile1.inmemory if result else result
+    result = not test_profile2.inmemory if result else result
+    result = not test_profile3.inmemory if result else result
+    result = not test_image.inmemory if result else result
+    result = test_system1.inmemory if result else result
+    result = not test_system2.inmemory if result else result
+
+    result = test_package.__dict__["_comment"] == "test comment" if result else result
+    result = test_file.__dict__["_comment"] == "test comment" if result else result
+    result = test_mgmtclass.__dict__["_comment"] == "test comment" if result else result
+    result = test_repo.__dict__["_comment"] == "test comment" if result else result
+    result = test_menu1.__dict__["_comment"] == "test comment" if result else result
+    result = test_menu2.__dict__["_comment"] == "" if result else result
+    result = test_distro.__dict__["_comment"] == "test comment" if result else result
+    result = test_profile1.__dict__["_comment"] == "test comment" if result else result
+    result = test_profile2.__dict__["_comment"] == "" if result else result
+    result = test_profile3.__dict__["_comment"] == "" if result else result
+    result = test_image.__dict__["_comment"] == "" if result else result
+    result = test_system1.__dict__["_comment"] == "test comment" if result else result
+    result = test_system2.__dict__["_comment"] == "" if result else result
+
+    # Cleanup
+    inmemory_api.remove_system(test_system1)
+    inmemory_api.remove_system(test_system2)
+    inmemory_api.remove_image(test_image)
+    inmemory_api.remove_profile(test_profile3)
+    inmemory_api.remove_profile(test_profile2)
+    inmemory_api.remove_profile(test_profile1)
+    inmemory_api.remove_distro(test_distro)
+    inmemory_api.remove_menu(test_menu2)
+    inmemory_api.remove_menu(test_menu1)
+    inmemory_api.remove_repo(test_repo)
+    inmemory_api.remove_mgmtclass(test_mgmtclass)
+    inmemory_api.remove_package(test_package)
+    inmemory_api.remove_file(test_file)
+
+    # Assert
+    assert result
diff --git a/tests/items/item_test.py b/tests/items/item_test.py
index b0898ed228..424a6ba9d5 100644
--- a/tests/items/item_test.py
+++ b/tests/items/item_test.py
@@ -454,7 +454,7 @@ def test_dump_vars(cobbler_api):
     print(result)
     assert "default_ownership" in result
     assert "owners" in result
-    assert len(result) == 152
+    assert len(result) == 153
 
 
 @pytest.mark.parametrize(
diff --git a/tests/modules/serializer/file_test.py b/tests/modules/serializer/file_test.py
index cba813a561..1ad23203ea 100644
--- a/tests/modules/serializer/file_test.py
+++ b/tests/modules/serializer/file_test.py
@@ -9,6 +9,7 @@
 from cobbler.modules.serializers import file
 from cobbler.cexceptions import CX
 from cobbler.settings import Settings
+from tests.conftest import does_not_raise
 
 
 @pytest.fixture()
@@ -242,3 +243,42 @@ def test_deserialize(
     # Assert
     assert stub_from.called
     stub_from.assert_called_with(expected_result)
+
+
+@pytest.mark.parametrize(
+    "input_collection_type,input_item,expected_result,expected_exception",
+    [
+        (
+            "distros",
+            {"name": "test"},
+            {"name": "test", "inmemory": True},
+            does_not_raise(),
+        ),
+        (
+            "distros",
+            {"name": "test"},
+            {"name": "fake", "inmemory": True},
+            pytest.raises(CX),
+        ),
+    ],
+)
+def test_deserialize_item(
+    mocker,
+    input_collection_type,
+    input_item,
+    expected_result,
+    expected_exception,
+    serializer_obj,
+):
+    # Arrange
+    mocked_input = mocker.mock_open(read_data=json.dumps(input_item))()
+    mocker.patch("builtins.open", return_value=mocked_input)
+
+    # Act
+    with expected_exception:
+        result = serializer_obj.deserialize_item(
+            input_collection_type, expected_result["name"]
+        )
+
+        # Assert
+        assert result == expected_result
diff --git a/tests/modules/serializer/mongodb_test.py b/tests/modules/serializer/mongodb_test.py
index ef715f14af..baad9dfb69 100644
--- a/tests/modules/serializer/mongodb_test.py
+++ b/tests/modules/serializer/mongodb_test.py
@@ -1,6 +1,11 @@
+from typing import ContextManager
 import pytest
 
+import copy
+
 from cobbler.modules.serializers import mongodb
+from cobbler.cexceptions import CX
+from tests.conftest import does_not_raise
 
 
 @pytest.fixture
@@ -108,3 +113,38 @@ def test_deserialize(mocker, mongodb_obj):
     assert mock_deserialize_raw.call_count == 1
     assert mock_collection.from_list.call_count == 1
     mock_collection.from_list.assert_called_with(return_deserialize_raw)
+
+
+@pytest.mark.parametrize(
+    "item_name,expected_exception",
+    [
+        (
+            "testitem",
+            does_not_raise(),
+        ),
+        (
+            None,
+            pytest.raises(CX),
+        ),
+    ],
+)
+def test_deserialize_item(
+    mongodb_obj: mongodb.MongoDBSerializer, item_name: str, expected_exception
+):
+    # Arrange
+    collection_type = "distro"
+    input_value = {"name": item_name, "arch": "x86_64"}
+    test_item = copy.deepcopy(input_value)
+    if item_name is not None:
+        mongodb_obj.mongodb["cobbler"][collection_type].insert_one(test_item)
+
+    expected_value = input_value.copy()
+    expected_value["inmemory"] = True
+
+    # Act
+    with expected_exception:
+        result = mongodb_obj.deserialize_item(collection_type, item_name)
+        print(result)
+
+        # Assert
+        assert result in (expected_value, {"inmemory": True})
diff --git a/tests/serializer_test.py b/tests/serializer_test.py
index 7d1310a88f..0d0db700c0 100644
--- a/tests/serializer_test.py
+++ b/tests/serializer_test.py
@@ -81,3 +81,18 @@ def test_deserialize(mocker, serializer_obj):
 
     # Assert
     storage_object_mock.deserialize.assert_called_with(input_collection, True)
+
+
+def test_deserialize_item(mocker, serializer_obj):
+    # Arrange
+    storage_object_mock = mocker.patch.object(serializer_obj, "storage_object")
+    input_collection_type = mocker.MagicMock()
+    input_name = mocker.MagicMock()
+
+    # Act
+    serializer_obj.deserialize_item(input_collection_type, input_name)
+
+    # Assert
+    storage_object_mock.deserialize_item.assert_called_with(
+        input_collection_type, input_name
+    )
diff --git a/tests/settings/migrations/normalize_test.py b/tests/settings/migrations/normalize_test.py
index 3b83f42faa..035d871362 100644
--- a/tests/settings/migrations/normalize_test.py
+++ b/tests/settings/migrations/normalize_test.py
@@ -192,4 +192,5 @@ def test_normalize_v3_4_0_full():
     assert new_settings["mongodb"] == {"host": "localhost", "port": 27017}
     assert "cache_enabled" in new_settings
     assert new_settings["cache_enabled"] == False
-    assert len(V3_4_0.normalize(new_settings)) == 134
+    assert new_settings["lazy_start"] == False
+    assert len(V3_4_0.normalize(new_settings)) == 135
diff --git a/tests/test_data/V3_4_0/settings.yaml b/tests/test_data/V3_4_0/settings.yaml
index 9c08ab18f4..308805974c 100644
--- a/tests/test_data/V3_4_0/settings.yaml
+++ b/tests/test_data/V3_4_0/settings.yaml
@@ -622,5 +622,15 @@ mongodb:
 # editing objects.
 cache_enabled: false
 
+
+# Set to true to speed up the start of the Cobbler. When storing collections as files, the directory with the names
+# of the collection elements will be scanned without reading and parsing the files themselves. In the case of storing
+# collections in the database, a projection query is made that includes only the names of the collection elements.
+# The first time an attribute of an element other than a name is accessed, a full read of all other attributes will be
+# performed, and a recursive full read of all elements on which this element depends. At startup, a background task is
+# also launched, which, when idle, fills in all the properties of the elements of the collections.
+# Suitable for configurations with a large number of elements placed on a slow device (HDD, network).
+lazy_start: false
+
 # list settings keys to exclude from validation - helpful to validate and migrate custom settings
 extra_settings_list: []
diff --git a/tests/xmlrpcapi/conftest.py b/tests/xmlrpcapi/conftest.py
index 6a66f90417..6d20500f0c 100644
--- a/tests/xmlrpcapi/conftest.py
+++ b/tests/xmlrpcapi/conftest.py
@@ -286,8 +286,8 @@ def remove_testdistro(remote: CobblerXMLRPCInterface, token: str):
 def create_testdistro(
     remote: CobblerXMLRPCInterface,
     token: str,
-    fk_kernel,
-    fk_initrd,
+    fk_kernel: str,
+    fk_initrd: str,
     create_kernel_initrd: Callable[[str, str], str],
 ):
     """
diff --git a/tests/xmlrpcapi/system_test.py b/tests/xmlrpcapi/system_test.py
index 6849672a83..2984ce75a8 100644
--- a/tests/xmlrpcapi/system_test.py
+++ b/tests/xmlrpcapi/system_test.py
@@ -286,10 +286,6 @@ def test_rename_interface(self, remote, token):
         "create_testmenu",
         "create_testprofile",
         "create_testsystem",
-        "remove_testdistro",
-        "remove_testmenu",
-        "remove_testprofile",
-        "remove_testsystem",
     )
     def test_copy_system(self, remote, token):
         """
@@ -304,9 +300,6 @@ def test_copy_system(self, remote, token):
         # Assert
         assert result
 
-        # Cleanup
-        remote.remove_system("testsytemcopy", token)
-
     @pytest.mark.usefixtures(
         "create_testdistro",
         "create_testmenu",