diff --git a/src/main/java/net/hostsharing/hsadminng/errors/CustomErrorResponse.java b/src/main/java/net/hostsharing/hsadminng/errors/CustomErrorResponse.java
index 2714b817..9b182137 100644
--- a/src/main/java/net/hostsharing/hsadminng/errors/CustomErrorResponse.java
+++ b/src/main/java/net/hostsharing/hsadminng/errors/CustomErrorResponse.java
@@ -9,7 +9,7 @@ import org.springframework.web.context.request.WebRequest;
 import java.time.LocalDateTime;
 
 @Getter
-class CustomErrorResponse {
+public class CustomErrorResponse {
 
     static ResponseEntity<CustomErrorResponse> errorResponse(
             final WebRequest request,
diff --git a/src/main/java/net/hostsharing/hsadminng/errors/MultiValidationException.java b/src/main/java/net/hostsharing/hsadminng/errors/MultiValidationException.java
new file mode 100644
index 00000000..9a6d459d
--- /dev/null
+++ b/src/main/java/net/hostsharing/hsadminng/errors/MultiValidationException.java
@@ -0,0 +1,19 @@
+package net.hostsharing.hsadminng.errors;
+
+import jakarta.validation.ValidationException;
+import java.util.List;
+
+import static java.lang.String.join;
+
+public class MultiValidationException extends ValidationException {
+
+    private MultiValidationException(final List<String> violations) {
+        super("[\n" + join(",\n", violations) + "\n]");
+    }
+
+    public static void throwInvalid(final List<String> violations) {
+        if (!violations.isEmpty()) {
+            throw new MultiValidationException(violations);
+        }
+    }
+}
diff --git a/src/main/java/net/hostsharing/hsadminng/errors/RestResponseEntityExceptionHandler.java b/src/main/java/net/hostsharing/hsadminng/errors/RestResponseEntityExceptionHandler.java
index 5d675484..d4d6e8bf 100644
--- a/src/main/java/net/hostsharing/hsadminng/errors/RestResponseEntityExceptionHandler.java
+++ b/src/main/java/net/hostsharing/hsadminng/errors/RestResponseEntityExceptionHandler.java
@@ -73,9 +73,10 @@ public class RestResponseEntityExceptionHandler
     }
 
     @ExceptionHandler({ Iban4jException.class, ValidationException.class })
-    protected ResponseEntity<CustomErrorResponse> handleIbanAndBicExceptions(
+    protected ResponseEntity<CustomErrorResponse> handleValidationExceptions(
             final Throwable exc, final WebRequest request) {
-        final var message = line(NestedExceptionUtils.getMostSpecificCause(exc).getMessage(), 0);
+        final String fullMessage = NestedExceptionUtils.getMostSpecificCause(exc).getMessage();
+        final var message = exc instanceof MultiValidationException ? fullMessage : line(fullMessage, 0);
         return errorResponse(request, HttpStatus.BAD_REQUEST, message);
     }
 
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemController.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemController.java
index 2ada5e0c..1343378c 100644
--- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemController.java
+++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemController.java
@@ -5,6 +5,7 @@ import net.hostsharing.hsadminng.hs.booking.generated.api.v1.api.HsBookingItemsA
 import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemInsertResource;
 import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemPatchResource;
 import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemResource;
+import net.hostsharing.hsadminng.hs.booking.item.validators.HsBookingItemEntityValidatorRegistry;
 import net.hostsharing.hsadminng.mapper.KeyValueMap;
 import net.hostsharing.hsadminng.mapper.Mapper;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -13,11 +14,12 @@ import org.springframework.transaction.annotation.Transactional;
 import org.springframework.web.bind.annotation.RestController;
 import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
 
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.PersistenceContext;
 import java.util.List;
 import java.util.UUID;
 import java.util.function.BiConsumer;
 
-import static net.hostsharing.hsadminng.hs.booking.item.validators.HsBookingItemEntityValidators.valid;
 import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange;
 
 @RestController
@@ -32,6 +34,9 @@ public class HsBookingItemController implements HsBookingItemsApi {
     @Autowired
     private HsBookingItemRepository bookingItemRepo;
 
+    @PersistenceContext
+    private EntityManager em;
+
     @Override
     @Transactional(readOnly = true)
     public ResponseEntity<List<HsBookingItemResource>> listBookingItemsByProjectUuid(
@@ -57,7 +62,7 @@ public class HsBookingItemController implements HsBookingItemsApi {
 
         final var entityToSave = mapper.map(body, HsBookingItemEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER);
 
-        final var saved = bookingItemRepo.save(valid(entityToSave));
+        final var saved = HsBookingItemEntityValidatorRegistry.validated(bookingItemRepo.save(entityToSave));
 
         final var uri =
                 MvcUriComponentsBuilder.fromController(getClass())
@@ -78,6 +83,7 @@ public class HsBookingItemController implements HsBookingItemsApi {
         context.define(currentUser, assumedRoles);
 
         final var result = bookingItemRepo.findByUuid(bookingItemUuid);
+        result.ifPresent(entity -> em.detach(entity)); // prevent further LAZY-loading
         return result
                 .map(bookingItemEntity -> ResponseEntity.ok(
                         mapper.map(bookingItemEntity, HsBookingItemResource.class, ENTITY_TO_RESOURCE_POSTMAPPER)))
@@ -112,7 +118,7 @@ public class HsBookingItemController implements HsBookingItemsApi {
 
         new HsBookingItemEntityPatcher(current).apply(body);
 
-        final var saved = bookingItemRepo.save(valid(current));
+        final var saved = bookingItemRepo.save(HsBookingItemEntityValidatorRegistry.validated(current));
         final var mapped = mapper.map(saved, HsBookingItemResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
         return ResponseEntity.ok(mapped);
     }
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java
index 1c5040e7..b820c243 100644
--- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java
+++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java
@@ -10,7 +10,7 @@ import lombok.Getter;
 import lombok.NoArgsConstructor;
 import lombok.Setter;
 import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectEntity;
-import net.hostsharing.hsadminng.hs.validation.Validatable;
+import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
 import net.hostsharing.hsadminng.mapper.PatchableMapWrapper;
 import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
 import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL;
@@ -19,6 +19,7 @@ import net.hostsharing.hsadminng.stringify.Stringify;
 import net.hostsharing.hsadminng.stringify.Stringifyable;
 import org.hibernate.annotations.Type;
 
+import jakarta.persistence.CascadeType;
 import jakarta.persistence.Column;
 import jakarta.persistence.Entity;
 import jakarta.persistence.EnumType;
@@ -27,12 +28,14 @@ import jakarta.persistence.GeneratedValue;
 import jakarta.persistence.Id;
 import jakarta.persistence.JoinColumn;
 import jakarta.persistence.ManyToOne;
+import jakarta.persistence.OneToMany;
 import jakarta.persistence.Table;
 import jakarta.persistence.Transient;
 import jakarta.persistence.Version;
 import java.io.IOException;
 import java.time.LocalDate;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.UUID;
 
@@ -62,7 +65,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
 @Setter
 @NoArgsConstructor
 @AllArgsConstructor
-public class HsBookingItemEntity implements Stringifyable, RbacObject, Validatable<HsBookingItemEntity, HsBookingItemType> {
+public class HsBookingItemEntity implements Stringifyable, RbacObject {
 
     private static Stringify<HsBookingItemEntity> stringify = stringify(HsBookingItemEntity.class)
             .withProp(HsBookingItemEntity::getProject)
@@ -105,6 +108,14 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject, Validatab
     @Column(columnDefinition = "resources")
     private Map<String, Object> resources = new HashMap<>();
 
+    @OneToMany(cascade = CascadeType.REFRESH, orphanRemoval = true)
+    @JoinColumn(name="parentitemuuid", referencedColumnName="uuid")
+    private List<HsBookingItemEntity> subBookingItems;
+
+    @OneToMany(cascade = CascadeType.REFRESH, orphanRemoval = true)
+    @JoinColumn(name="bookingitemuuid", referencedColumnName="uuid")
+    private List<HsHostingAssetEntity> subHostingAssets;
+
     @Transient
     private PatchableMapWrapper<Object> resourcesWrapper;
 
@@ -150,16 +161,6 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject, Validatab
         return parentItem == null ? null : parentItem.relatedProject();
     }
 
-    @Override
-    public String getPropertiesName() {
-        return "resources";
-    }
-
-    @Override
-    public Map<String, Object> getProperties() {
-        return resources;
-    }
-
     public HsBookingProjectEntity getRelatedProject() {
         return project != null ? project : parentItem.getRelatedProject();
     }
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidator.java
new file mode 100644
index 00000000..7d002bac
--- /dev/null
+++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidator.java
@@ -0,0 +1,77 @@
+package net.hostsharing.hsadminng.hs.booking.item.validators;
+
+import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
+import net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
+import net.hostsharing.hsadminng.hs.validation.ValidatableProperty;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Stream;
+
+import static java.util.Arrays.stream;
+import static java.util.Collections.emptyList;
+import static java.util.Optional.ofNullable;
+
+public class HsBookingItemEntityValidator extends HsEntityValidator<HsBookingItemEntity> {
+
+    public HsBookingItemEntityValidator(final ValidatableProperty<?>... properties) {
+        super(properties);
+    }
+
+    public List<String> validate(final HsBookingItemEntity bookingItem) {
+        return sequentiallyValidate(
+                () -> validateProperties(bookingItem),
+                () -> optionallyValidate(bookingItem.getParentItem()),
+                () -> validateAgainstSubEntities(bookingItem)
+        );
+    }
+
+    private List<String> validateProperties(final HsBookingItemEntity bookingItem) {
+        return enrich(prefix(bookingItem.toShortString(), "resources"), validateProperties(bookingItem.getResources()));
+    }
+
+    private static List<String> optionallyValidate(final HsBookingItemEntity bookingItem) {
+           return bookingItem != null
+                   ? enrich(prefix(bookingItem.toShortString(), ""),
+                        HsBookingItemEntityValidatorRegistry.doValidate(bookingItem))
+                   : emptyList();
+    }
+
+    protected List<String> validateAgainstSubEntities(final HsBookingItemEntity bookingItem) {
+        return enrich(prefix(bookingItem.toShortString(), "resources"),
+                    Stream.concat(
+                        stream(propertyValidators)
+                                .map(propDef -> propDef.validateTotals(bookingItem))
+                                .flatMap(Collection::stream),
+                        stream(propertyValidators)
+                                .filter(ValidatableProperty::isTotalsValidator)
+                                .map(prop -> validateMaxTotalValue(bookingItem, prop))
+                ).filter(Objects::nonNull).toList());
+    }
+
+    // TODO.refa: convert into generic shape like multi-options validator
+    private static String validateMaxTotalValue(
+            final HsBookingItemEntity bookingItem,
+            final ValidatableProperty<?> propDef) {
+        final var propName = propDef.propertyName();
+        final var propUnit = ofNullable(propDef.unit()).map(u -> " " + u).orElse("");
+        final var totalValue = ofNullable(bookingItem.getSubBookingItems()).orElse(emptyList())
+                .stream()
+                .map(subItem -> propDef.getValue(subItem.getResources()))
+                .map(HsBookingItemEntityValidator::toNonNullInteger)
+                .reduce(0, Integer::sum);
+        final var maxValue = getNonNullIntegerValue(propDef, bookingItem.getResources());
+        if (propDef.thresholdPercentage() != null ) {
+            return totalValue > (maxValue * propDef.thresholdPercentage() / 100)
+                    ? "%s' maximum total is %d%s, but actual total %s %d%s, which exceeds threshold of %d%%"
+                        .formatted(propName, maxValue, propUnit, propName, totalValue, propUnit, propDef.thresholdPercentage())
+                    : null;
+        } else {
+            return totalValue > maxValue
+                    ? "%s' maximum total is %d%s, but actual total %s %d%s"
+                    .formatted(propName, maxValue, propUnit, propName, totalValue, propUnit)
+                    : null;
+        }
+    }
+}
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidators.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidatorRegistry.java
similarity index 56%
rename from src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidators.java
rename to src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidatorRegistry.java
index 1f4493e2..e067781e 100644
--- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidators.java
+++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidatorRegistry.java
@@ -1,12 +1,12 @@
 package net.hostsharing.hsadminng.hs.booking.item.validators;
 
-import lombok.experimental.UtilityClass;
 import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
 import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType;
 import net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
+import net.hostsharing.hsadminng.errors.MultiValidationException;
 
-import jakarta.validation.ValidationException;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
@@ -14,37 +14,42 @@ import static java.util.Arrays.stream;
 import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.CLOUD_SERVER;
 import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGED_SERVER;
 import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGED_WEBSPACE;
+import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.PRIVATE_CLOUD;
 
-@UtilityClass
-public class HsBookingItemEntityValidators {
+public class HsBookingItemEntityValidatorRegistry {
 
-    private static final Map<Enum<HsBookingItemType>, HsEntityValidator<HsBookingItemEntity, HsBookingItemType>> validators = new HashMap<>();
+    private static final Map<Enum<HsBookingItemType>, HsEntityValidator<HsBookingItemEntity>> validators = new HashMap<>();
     static {
+        register(PRIVATE_CLOUD, new HsPrivateCloudBookingItemValidator());
         register(CLOUD_SERVER, new HsCloudServerBookingItemValidator());
         register(MANAGED_SERVER, new HsManagedServerBookingItemValidator());
         register(MANAGED_WEBSPACE, new HsManagedWebspaceBookingItemValidator());
     }
 
-    private static void register(final Enum<HsBookingItemType> type, final HsEntityValidator<HsBookingItemEntity, HsBookingItemType> validator) {
+    private static void register(final Enum<HsBookingItemType> type, final HsEntityValidator<HsBookingItemEntity> validator) {
         stream(validator.propertyValidators).forEach( entry -> {
             entry.verifyConsistency(Map.entry(type, validator));
         });
         validators.put(type, validator);
     }
 
-    public static HsEntityValidator<HsBookingItemEntity, HsBookingItemType> forType(final Enum<HsBookingItemType> type) {
-        return validators.get(type);
+    public static HsEntityValidator<HsBookingItemEntity> forType(final Enum<HsBookingItemType> type) {
+        if ( validators.containsKey(type)) {
+            return validators.get(type);
+        }
+        throw new IllegalArgumentException("no validator found for type " + type);
     }
 
     public static  Set<Enum<HsBookingItemType>> types() {
         return validators.keySet();
     }
 
-    public static HsBookingItemEntity valid(final HsBookingItemEntity entityToSave) {
-        final var violations = HsBookingItemEntityValidators.forType(entityToSave.getType()).validate(entityToSave);
-        if (!violations.isEmpty()) {
-            throw new ValidationException(violations.toString());
-        }
+    public static List<String> doValidate(final HsBookingItemEntity bookingItem) {
+        return HsBookingItemEntityValidatorRegistry.forType(bookingItem.getType()).validate(bookingItem);
+    }
+
+    public static HsBookingItemEntity validated(final HsBookingItemEntity entityToSave) {
+        MultiValidationException.throwInvalid(doValidate(entityToSave));
         return entityToSave;
     }
 }
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsCloudServerBookingItemValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsCloudServerBookingItemValidator.java
index fa09f2c3..07bb80da 100644
--- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsCloudServerBookingItemValidator.java
+++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsCloudServerBookingItemValidator.java
@@ -1,20 +1,18 @@
 package net.hostsharing.hsadminng.hs.booking.item.validators;
 
-import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
-import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType;
-import net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
 
-import static net.hostsharing.hsadminng.hs.validation.EnumerationPropertyValidator.enumerationProperty;
-import static net.hostsharing.hsadminng.hs.validation.IntegerPropertyValidator.integerProperty;
 
-class HsCloudServerBookingItemValidator extends HsEntityValidator<HsBookingItemEntity, HsBookingItemType> {
+import static net.hostsharing.hsadminng.hs.validation.EnumerationProperty.enumerationProperty;
+import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty;
+
+class HsCloudServerBookingItemValidator extends HsBookingItemEntityValidator {
 
     HsCloudServerBookingItemValidator() {
         super(
             integerProperty("CPUs").min(1).max(32).required(),
             integerProperty("RAM").unit("GB").min(1).max(128).required(),
             integerProperty("SSD").unit("GB").min(25).max(1000).step(25).required(),
-            integerProperty("HDD").unit("GB").min(0).max(4000).step(250).optional(),
+            integerProperty("HDD").unit("GB").min(0).max(4000).step(250).withDefault(0),
             integerProperty("Traffic").unit("GB").min(250).max(10000).step(250).required(),
             enumerationProperty("SLA-Infrastructure").values("BASIC", "EXT8H", "EXT4H", "EXT2H").optional()
         );
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedServerBookingItemValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedServerBookingItemValidator.java
index 79c41070..a267b104 100644
--- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedServerBookingItemValidator.java
+++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedServerBookingItemValidator.java
@@ -1,24 +1,22 @@
 package net.hostsharing.hsadminng.hs.booking.item.validators;
 
-import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
-import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType;
-import net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
 
-import static net.hostsharing.hsadminng.hs.validation.BooleanPropertyValidator.booleanProperty;
-import static net.hostsharing.hsadminng.hs.validation.EnumerationPropertyValidator.enumerationProperty;
-import static net.hostsharing.hsadminng.hs.validation.IntegerPropertyValidator.integerProperty;
 
-class HsManagedServerBookingItemValidator extends HsEntityValidator<HsBookingItemEntity, HsBookingItemType> {
+import static net.hostsharing.hsadminng.hs.validation.BooleanProperty.booleanProperty;
+import static net.hostsharing.hsadminng.hs.validation.EnumerationProperty.enumerationProperty;
+import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty;
+
+class HsManagedServerBookingItemValidator extends HsBookingItemEntityValidator {
 
     HsManagedServerBookingItemValidator() {
         super(
             integerProperty("CPUs").min(1).max(32).required(),
             integerProperty("RAM").unit("GB").min(1).max(128).required(),
-            integerProperty("SSD").unit("GB").min(25).max(1000).step(25).required(),
-            integerProperty("HDD").unit("GB").min(0).max(4000).step(250).optional(),
-            integerProperty("Traffic").unit("GB").min(250).max(10000).step(250).required(),
-            enumerationProperty("SLA-Platform").values("BASIC", "EXT8H", "EXT4H", "EXT2H").optional(),
-            booleanProperty("SLA-EMail").falseIf("SLA-Platform", "BASIC").optional(),
+            integerProperty("SSD").unit("GB").min(25).max(1000).step(25).required().asTotalLimit().withThreshold(200),
+            integerProperty("HDD").unit("GB").min(0).max(4000).step(250).withDefault(0).asTotalLimit().withThreshold(200),
+            integerProperty("Traffic").unit("GB").min(250).max(10000).step(250).required().asTotalLimit().withThreshold(200),
+            enumerationProperty("SLA-Platform").values("BASIC", "EXT8H", "EXT4H", "EXT2H").withDefault("BASIC"),
+            booleanProperty("SLA-EMail").falseIf("SLA-Platform", "BASIC").withDefault(false),
             booleanProperty("SLA-Maria").falseIf("SLA-Platform", "BASIC").optional(),
             booleanProperty("SLA-PgSQL").falseIf("SLA-Platform", "BASIC").optional(),
             booleanProperty("SLA-Office").falseIf("SLA-Platform", "BASIC").optional(),
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedWebspaceBookingItemValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedWebspaceBookingItemValidator.java
index 482d0900..bf637f15 100644
--- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedWebspaceBookingItemValidator.java
+++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedWebspaceBookingItemValidator.java
@@ -1,24 +1,98 @@
 package net.hostsharing.hsadminng.hs.booking.item.validators;
 
 import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
-import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType;
-import net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
+import net.hostsharing.hsadminng.hs.validation.IntegerProperty;
+import org.apache.commons.lang3.function.TriFunction;
 
+import java.util.List;
 
-import static net.hostsharing.hsadminng.hs.validation.BooleanPropertyValidator.booleanProperty;
-import static net.hostsharing.hsadminng.hs.validation.EnumerationPropertyValidator.enumerationProperty;
-import static net.hostsharing.hsadminng.hs.validation.IntegerPropertyValidator.integerProperty;
+import static java.util.Collections.emptyList;
+import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_EMAIL_SETUP;
+import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.EMAIL_ADDRESS;
+import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MARIADB_DATABASE;
+import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MARIADB_USER;
+import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.PGSQL_DATABASE;
+import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.PGSQL_USER;
+import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.UNIX_USER;
+import static net.hostsharing.hsadminng.hs.validation.BooleanProperty.booleanProperty;
+import static net.hostsharing.hsadminng.hs.validation.EnumerationProperty.enumerationProperty;
+import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty;
 
-class HsManagedWebspaceBookingItemValidator extends HsEntityValidator<HsBookingItemEntity, HsBookingItemType> {
+class HsManagedWebspaceBookingItemValidator extends HsBookingItemEntityValidator {
 
     public HsManagedWebspaceBookingItemValidator() {
         super(
             integerProperty("SSD").unit("GB").min(1).max(100).step(1).required(),
             integerProperty("HDD").unit("GB").min(0).max(250).step(10).optional(),
             integerProperty("Traffic").unit("GB").min(10).max(1000).step(10).required(),
-            enumerationProperty("SLA-Platform").values("BASIC", "EXT24H").optional(),
-            integerProperty("Daemons").min(0).max(10).optional(),
-            booleanProperty("Online Office Server").optional()
+            integerProperty("Multi").min(1).max(100).step(1).withDefault(1)
+                    .eachComprising( 25, unixUsers())
+                    .eachComprising(  5, databaseUsers())
+                    .eachComprising(  5, databases())
+                    .eachComprising(250, eMailAddresses()),
+            integerProperty("Daemons").min(0).max(10).withDefault(0),
+            booleanProperty("Online Office Server").optional(),
+            enumerationProperty("SLA-Platform").values("BASIC", "EXT24H").withDefault("BASIC")
         );
     }
+
+    private static TriFunction<HsBookingItemEntity, IntegerProperty, Integer, List<String>> unixUsers() {
+        return (final HsBookingItemEntity entity, final IntegerProperty prop, final Integer factor) -> {
+            final var unixUserCount = entity.getSubHostingAssets().stream()
+                    .flatMap(ha -> ha.getSubHostingAssets().stream())
+                    .filter(ha -> ha.getType() == UNIX_USER)
+                    .count();
+            final long limitingValue = prop.getValue(entity.getResources());
+            if (unixUserCount > factor*limitingValue) {
+                return List.of(prop.propertyName() + "=" + limitingValue + " allows at maximum " + limitingValue*factor + " unix users, but " + unixUserCount + " found");
+            }
+            return emptyList();
+        };
+    }
+
+    private static TriFunction<HsBookingItemEntity, IntegerProperty, Integer, List<String>> databaseUsers() {
+        return (final HsBookingItemEntity entity, final IntegerProperty prop, final Integer factor) -> {
+            final var unixUserCount = entity.getSubHostingAssets().stream()
+                    .flatMap(ha -> ha.getSubHostingAssets().stream())
+                    .filter(bi -> bi.getType() == PGSQL_USER || bi.getType() == MARIADB_USER )
+                    .count();
+            final long limitingValue = prop.getValue(entity.getResources());
+            if (unixUserCount > factor*limitingValue) {
+                return List.of(prop.propertyName() + "=" + limitingValue + " allows at maximum " + limitingValue*factor + " database users, but " + unixUserCount + " found");
+            }
+            return emptyList();
+        };
+    }
+
+    private static TriFunction<HsBookingItemEntity, IntegerProperty, Integer, List<String>> databases() {
+        return (final HsBookingItemEntity entity, final IntegerProperty prop, final Integer factor) -> {
+            final var unixUserCount = entity.getSubHostingAssets().stream()
+                    .flatMap(ha -> ha.getSubHostingAssets().stream())
+                    .filter(bi -> bi.getType()==PGSQL_USER || bi.getType()==MARIADB_USER )
+                    .flatMap(domainEMailSetup -> domainEMailSetup.getSubHostingAssets().stream()
+                            .filter(ha -> ha.getType()==PGSQL_DATABASE || ha.getType()==MARIADB_DATABASE))
+                    .count();
+            final long limitingValue = prop.getValue(entity.getResources());
+            if (unixUserCount > factor*limitingValue) {
+                return List.of(prop.propertyName() + "=" + limitingValue + " allows at maximum " + limitingValue*factor + " databases, but " + unixUserCount + " found");
+            }
+            return emptyList();
+        };
+    }
+
+    private static TriFunction<HsBookingItemEntity, IntegerProperty, Integer, List<String>> eMailAddresses() {
+        return (final HsBookingItemEntity entity, final IntegerProperty prop, final Integer factor) -> {
+            final var unixUserCount = entity.getSubHostingAssets().stream()
+                    .flatMap(ha -> ha.getSubHostingAssets().stream())
+                    .filter(bi -> bi.getType() == DOMAIN_EMAIL_SETUP)
+                    .flatMap(domainEMailSetup -> domainEMailSetup.getSubHostingAssets().stream()
+                            .filter(ha -> ha.getType()==EMAIL_ADDRESS))
+                    .count();
+            final long limitingValue = prop.getValue(entity.getResources());
+            if (unixUserCount > factor*limitingValue) {
+                return List.of(prop.propertyName() + "=" + limitingValue + " allows at maximum " + limitingValue*factor + " databases, but " + unixUserCount + " found");
+            }
+            return emptyList();
+        };
+    }
 }
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsPrivateCloudBookingItemValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsPrivateCloudBookingItemValidator.java
new file mode 100644
index 00000000..317f2f0c
--- /dev/null
+++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsPrivateCloudBookingItemValidator.java
@@ -0,0 +1,18 @@
+package net.hostsharing.hsadminng.hs.booking.item.validators;
+
+import static net.hostsharing.hsadminng.hs.validation.EnumerationProperty.enumerationProperty;
+import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty;
+
+class HsPrivateCloudBookingItemValidator extends HsBookingItemEntityValidator {
+
+    HsPrivateCloudBookingItemValidator() {
+        super(
+            integerProperty("CPUs").min(4).max(128).required().asTotalLimit(),
+            integerProperty("RAM").unit("GB").min(4).max(512).required().asTotalLimit(),
+            integerProperty("SSD").unit("GB").min(100).max(4000).step(25).required().asTotalLimit(),
+            integerProperty("HDD").unit("GB").min(0).max(16000).step(25).withDefault(0).asTotalLimit(),
+            integerProperty("Traffic").unit("GB").min(1000).max(40000).step(250).required().asTotalLimit(),
+            enumerationProperty("SLA-Infrastructure").values("BASIC", "EXT8H", "EXT4H", "EXT2H").withDefault("BASIC")
+        );
+    }
+}
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java
index a645bb78..76003671 100644
--- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java
+++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java
@@ -20,7 +20,7 @@ import java.util.List;
 import java.util.UUID;
 import java.util.function.BiConsumer;
 
-import static net.hostsharing.hsadminng.hs.hosting.asset.validators.HsHostingAssetEntityValidators.valid;
+import static net.hostsharing.hsadminng.hs.hosting.asset.validators.HsHostingAssetEntityValidatorRegistry.validated;
 
 @RestController
 public class HsHostingAssetController implements HsHostingAssetsApi {
@@ -62,7 +62,7 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
 
         final var entityToSave = mapper.map(body, HsHostingAssetEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER);
 
-        final var saved = assetRepo.save(valid(entityToSave));
+        final var saved = validated(assetRepo.save(entityToSave));
 
         final var uri =
                 MvcUriComponentsBuilder.fromController(getClass())
@@ -117,7 +117,7 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
 
         new HsHostingAssetEntityPatcher(current).apply(body);
 
-        final var saved = assetRepo.save(valid(current));
+        final var saved = validated(assetRepo.save(current));
         final var mapped = mapper.map(saved, HsHostingAssetResource.class);
         return ResponseEntity.ok(mapped);
     }
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntity.java
index 8d573c48..3f8202ef 100644
--- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntity.java
+++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntity.java
@@ -8,7 +8,6 @@ import lombok.Getter;
 import lombok.NoArgsConstructor;
 import lombok.Setter;
 import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
-import net.hostsharing.hsadminng.hs.validation.Validatable;
 import net.hostsharing.hsadminng.mapper.PatchableMapWrapper;
 import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
 import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL;
@@ -17,6 +16,7 @@ import net.hostsharing.hsadminng.stringify.Stringify;
 import net.hostsharing.hsadminng.stringify.Stringifyable;
 import org.hibernate.annotations.Type;
 
+import jakarta.persistence.CascadeType;
 import jakarta.persistence.Column;
 import jakarta.persistence.Entity;
 import jakarta.persistence.EnumType;
@@ -25,11 +25,13 @@ import jakarta.persistence.GeneratedValue;
 import jakarta.persistence.Id;
 import jakarta.persistence.JoinColumn;
 import jakarta.persistence.ManyToOne;
+import jakarta.persistence.OneToMany;
 import jakarta.persistence.Table;
 import jakarta.persistence.Transient;
 import jakarta.persistence.Version;
 import java.io.IOException;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.UUID;
 
@@ -56,7 +58,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
 @Setter
 @NoArgsConstructor
 @AllArgsConstructor
-public class HsHostingAssetEntity implements Stringifyable, RbacObject, Validatable<HsHostingAssetEntity, HsHostingAssetType> {
+public class HsHostingAssetEntity implements Stringifyable, RbacObject {
 
     private static Stringify<HsHostingAssetEntity> stringify = stringify(HsHostingAssetEntity.class)
             .withProp(HsHostingAssetEntity::getType)
@@ -91,6 +93,10 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject, Validata
     @Enumerated(EnumType.STRING)
     private HsHostingAssetType type;
 
+    @OneToMany(cascade = CascadeType.REFRESH, orphanRemoval = true)
+    @JoinColumn(name="parentassetuuid", referencedColumnName="uuid")
+    private List<HsHostingAssetEntity> subHostingAssets;
+
     @Column(name = "identifier")
     private String identifier; // vm1234, xyz00, example.org, xyz00_abc
 
@@ -114,16 +120,6 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject, Validata
         PatchableMapWrapper.of(configWrapper, (newWrapper) -> {configWrapper = newWrapper; }, config).assign(newConfg);
     }
 
-    @Override
-    public String getPropertiesName() {
-        return "config";
-    }
-
-    @Override
-    public Map<String, Object> getProperties() {
-        return config;
-    }
-
     @Override
     public String toString() {
         return stringify.apply(this);
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsController.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsController.java
index 47852310..0da530bd 100644
--- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsController.java
+++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsController.java
@@ -1,6 +1,6 @@
 package net.hostsharing.hsadminng.hs.hosting.asset;
 
-import net.hostsharing.hsadminng.hs.hosting.asset.validators.HsHostingAssetEntityValidators;
+import net.hostsharing.hsadminng.hs.hosting.asset.validators.HsHostingAssetEntityValidatorRegistry;
 import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.api.HsHostingAssetPropsApi;
 import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetTypeResource;
 import org.springframework.http.ResponseEntity;
@@ -15,7 +15,7 @@ public class HsHostingAssetPropsController implements HsHostingAssetPropsApi {
 
     @Override
     public ResponseEntity<List<String>> listAssetTypes() {
-        final var resource = HsHostingAssetEntityValidators.types().stream()
+        final var resource = HsHostingAssetEntityValidatorRegistry.types().stream()
                 .map(Enum::name)
                 .toList();
         return ResponseEntity.ok(resource);
@@ -25,7 +25,8 @@ public class HsHostingAssetPropsController implements HsHostingAssetPropsApi {
     public ResponseEntity<List<Object>> listAssetTypeProps(
             final HsHostingAssetTypeResource assetType) {
 
-        final var propValidators = HsHostingAssetEntityValidators.forType(HsHostingAssetType.of(assetType));
+        final Enum<HsHostingAssetType> type = HsHostingAssetType.of(assetType);
+        final var propValidators = HsHostingAssetEntityValidatorRegistry.forType(type);
         final List<Map<String, Object>> resource = propValidators.properties();
         return ResponseEntity.ok(toListOfObjects(resource));
     }
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidator.java
new file mode 100644
index 00000000..3a0438ee
--- /dev/null
+++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidator.java
@@ -0,0 +1,76 @@
+package net.hostsharing.hsadminng.hs.hosting.asset.validators;
+
+import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
+import net.hostsharing.hsadminng.hs.booking.item.validators.HsBookingItemEntityValidatorRegistry;
+import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
+import net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
+import net.hostsharing.hsadminng.hs.validation.ValidatableProperty;
+
+import java.util.List;
+import java.util.Objects;
+
+import static java.util.Arrays.stream;
+import static java.util.Collections.emptyList;
+import static java.util.Optional.ofNullable;
+
+public class HsHostingAssetEntityValidator extends HsEntityValidator<HsHostingAssetEntity> {
+
+    public HsHostingAssetEntityValidator(final ValidatableProperty<?>... properties) {
+        super(properties);
+    }
+
+
+    @Override
+    public List<String> validate(final HsHostingAssetEntity assetEntity) {
+        return sequentiallyValidate(
+                () -> validateProperties(assetEntity),
+                () -> optionallyValidate(assetEntity.getBookingItem()),
+                () -> optionallyValidate(assetEntity.getParentAsset()),
+                () -> validateAgainstSubEntities(assetEntity)
+        );
+    }
+
+    private List<String> validateProperties(final HsHostingAssetEntity assetEntity) {
+        return enrich(prefix(assetEntity.toShortString(), "config"), validateProperties(assetEntity.getConfig()));
+    }
+
+    private static List<String> optionallyValidate(final HsHostingAssetEntity assetEntity) {
+        return assetEntity != null
+                ? enrich(prefix(assetEntity.toShortString(), "parentAsset"),
+                        HsHostingAssetEntityValidatorRegistry.forType(assetEntity.getType()).validate(assetEntity))
+                : emptyList();
+    }
+
+    private static List<String> optionallyValidate(final HsBookingItemEntity bookingItem) {
+        return bookingItem != null
+                ? enrich(prefix(bookingItem.toShortString(), "bookingItem"),
+                    HsBookingItemEntityValidatorRegistry.doValidate(bookingItem))
+                : emptyList();
+    }
+
+    protected List<String> validateAgainstSubEntities(final HsHostingAssetEntity assetEntity) {
+        return enrich(prefix(assetEntity.toShortString(), "config"),
+                stream(propertyValidators)
+                    .filter(ValidatableProperty::isTotalsValidator)
+                    .map(prop -> validateMaxTotalValue(assetEntity, prop))
+                    .filter(Objects::nonNull)
+                    .toList());
+    }
+
+    private String validateMaxTotalValue(
+            final HsHostingAssetEntity hostingAsset,
+            final ValidatableProperty<?> propDef) {
+        final var propName = propDef.propertyName();
+        final var propUnit = ofNullable(propDef.unit()).map(u -> " " + u).orElse("");
+        final var totalValue = ofNullable(hostingAsset.getSubHostingAssets()).orElse(emptyList())
+                .stream()
+                .map(subItem -> propDef.getValue(subItem.getConfig()))
+                .map(HsEntityValidator::toNonNullInteger)
+                .reduce(0, Integer::sum);
+        final var maxValue = getNonNullIntegerValue(propDef, hostingAsset.getConfig());
+        return totalValue > maxValue
+                ? "%s' maximum total is %d%s, but actual total is %s %d%s".formatted(
+                propName, maxValue, propUnit, propName, totalValue, propUnit)
+                : null;
+    }
+}
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java
new file mode 100644
index 00000000..a1cac8e0
--- /dev/null
+++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java
@@ -0,0 +1,50 @@
+package net.hostsharing.hsadminng.hs.hosting.asset.validators;
+
+import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
+import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType;
+import net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
+import net.hostsharing.hsadminng.errors.MultiValidationException;
+
+import java.util.*;
+
+import static java.util.Arrays.stream;
+import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.*;
+
+public class HsHostingAssetEntityValidatorRegistry {
+
+    private static final Map<Enum<HsHostingAssetType>, HsEntityValidator<HsHostingAssetEntity>> validators = new HashMap<>();
+    static {
+        register(CLOUD_SERVER, new HsHostingAssetEntityValidator());
+        register(MANAGED_SERVER, new HsManagedServerHostingAssetValidator());
+        register(MANAGED_WEBSPACE, new HsManagedWebspaceHostingAssetValidator());
+        register(UNIX_USER, new HsHostingAssetEntityValidator());
+    }
+
+    private static void register(final Enum<HsHostingAssetType> type, final HsEntityValidator<HsHostingAssetEntity> validator) {
+        stream(validator.propertyValidators).forEach( entry -> {
+            entry.verifyConsistency(Map.entry(type, validator));
+        });
+        validators.put(type, validator);
+    }
+
+    public static HsEntityValidator<HsHostingAssetEntity> forType(final Enum<HsHostingAssetType> type) {
+        if ( validators.containsKey(type)) {
+            return validators.get(type);
+        }
+        throw new IllegalArgumentException("no validator found for type " + type);
+    }
+
+    public static Set<Enum<HsHostingAssetType>> types() {
+        return validators.keySet();
+    }
+
+    public static List<String> doValidate(final HsHostingAssetEntity hostingAsset) {
+        return HsHostingAssetEntityValidatorRegistry.forType(hostingAsset.getType()).validate(hostingAsset);
+    }
+
+    public static HsHostingAssetEntity validated(final HsHostingAssetEntity entityToSave) {
+        MultiValidationException.throwInvalid(doValidate(entityToSave));
+        return entityToSave;
+    }
+
+}
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidators.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidators.java
deleted file mode 100644
index 11df9a84..00000000
--- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidators.java
+++ /dev/null
@@ -1,51 +0,0 @@
-package net.hostsharing.hsadminng.hs.hosting.asset.validators;
-
-import lombok.experimental.UtilityClass;
-import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
-import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType;
-import net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
-
-import jakarta.validation.ValidationException;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Set;
-
-import static java.util.Arrays.stream;
-import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.CLOUD_SERVER;
-import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_SERVER;
-import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_WEBSPACE;
-
-@UtilityClass
-public class HsHostingAssetEntityValidators {
-
-    private static final Map<Enum<HsHostingAssetType>, HsEntityValidator<HsHostingAssetEntity, HsHostingAssetType>> validators = new HashMap<>();
-    static {
-        register(CLOUD_SERVER, new HsEntityValidator<>());
-        register(MANAGED_SERVER, new HsManagedServerHostingAssetValidator());
-        register(MANAGED_WEBSPACE, new HsManagedWebspaceHostingAssetValidator());
-    }
-
-    private static void register(final Enum<HsHostingAssetType> type, final HsEntityValidator<HsHostingAssetEntity, HsHostingAssetType> validator) {
-        stream(validator.propertyValidators).forEach( entry -> {
-            entry.verifyConsistency(Map.entry(type, validator));
-        });
-        validators.put(type, validator);
-    }
-
-    public static HsEntityValidator<HsHostingAssetEntity, HsHostingAssetType> forType(final Enum<HsHostingAssetType> type) {
-        return validators.get(type);
-    }
-
-    public static Set<Enum<HsHostingAssetType>> types() {
-        return validators.keySet();
-    }
-
-
-    public static HsHostingAssetEntity valid(final HsHostingAssetEntity entityToSave) {
-        final var violations = HsHostingAssetEntityValidators.forType(entityToSave.getType()).validate(entityToSave);
-        if (!violations.isEmpty()) {
-            throw new ValidationException(violations.toString());
-        }
-        return entityToSave;
-    }
-}
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidator.java
index 35f3b81d..b2107866 100644
--- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidator.java
+++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidator.java
@@ -1,12 +1,8 @@
 package net.hostsharing.hsadminng.hs.hosting.asset.validators;
 
-import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
-import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType;
-import net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
+import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty;
 
-import static net.hostsharing.hsadminng.hs.validation.IntegerPropertyValidator.integerProperty;
-
-class HsManagedServerHostingAssetValidator extends HsEntityValidator<HsHostingAssetEntity, HsHostingAssetType> {
+class HsManagedServerHostingAssetValidator extends HsHostingAssetEntityValidator {
 
     public HsManagedServerHostingAssetValidator() {
         super(
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedWebspaceHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedWebspaceHostingAssetValidator.java
index ffef39d7..19c9dc24 100644
--- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedWebspaceHostingAssetValidator.java
+++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedWebspaceHostingAssetValidator.java
@@ -1,28 +1,29 @@
 package net.hostsharing.hsadminng.hs.hosting.asset.validators;
 
 import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
-import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType;
-import net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
 
+import java.util.Collection;
+import java.util.stream.Stream;
+import java.util.Collections;
 import java.util.List;
+import java.util.stream.Collectors;
 
-
-class HsManagedWebspaceHostingAssetValidator extends HsEntityValidator<HsHostingAssetEntity, HsHostingAssetType> {
+class HsManagedWebspaceHostingAssetValidator extends HsHostingAssetEntityValidator {
     public HsManagedWebspaceHostingAssetValidator() {
     }
 
     @Override
     public List<String> validate(final HsHostingAssetEntity assetEntity) {
-        final var result = super.validate(assetEntity);
-        validateIdentifierPattern(result, assetEntity);
-
-        return result;
+        return Stream.of(validateIdentifierPattern(assetEntity), super.validate(assetEntity))
+                .flatMap(Collection::stream)
+                .collect(Collectors.toList());
     }
 
-    private static void validateIdentifierPattern(final List<String> result, final HsHostingAssetEntity assetEntity) {
+    private static List<String> validateIdentifierPattern(final HsHostingAssetEntity assetEntity) {
         final var expectedIdentifierPattern = "^" + assetEntity.getParentAsset().getBookingItem().getProject().getDebitor().getDefaultPrefix() + "[0-9][0-9]$";
         if ( !assetEntity.getIdentifier().matches(expectedIdentifierPattern)) {
-            result.add("'identifier' expected to match '"+expectedIdentifierPattern+"', but is '" + assetEntity.getIdentifier() + "'");
+            return List.of("'identifier' expected to match '"+expectedIdentifierPattern+"', but is '" + assetEntity.getIdentifier() + "'");
         }
+        return Collections.emptyList();
     }
 }
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionController.java
index a22065c0..6279ad05 100644
--- a/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionController.java
+++ b/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionController.java
@@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.hs.office.coopassets;
 import net.hostsharing.hsadminng.context.Context;
 import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeCoopAssetsApi;
 import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.*;
+import net.hostsharing.hsadminng.errors.MultiValidationException;
 import net.hostsharing.hsadminng.mapper.Mapper;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.format.annotation.DateTimeFormat;
@@ -13,14 +14,12 @@ import org.springframework.web.bind.annotation.RestController;
 import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
 
 import jakarta.persistence.EntityNotFoundException;
-import jakarta.validation.ValidationException;
 import java.time.LocalDate;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.UUID;
 import java.util.function.BiConsumer;
 
-import static java.lang.String.join;
 import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopAssetsTransactionTypeResource.*;
 
 @RestController
@@ -97,9 +96,7 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
         validateDebitTransaction(requestBody, violations);
         validateCreditTransaction(requestBody, violations);
         validateAssetValue(requestBody, violations);
-        if (violations.size() > 0) {
-            throw new ValidationException("[" + join(", ", violations) + "]");
-        }
+        MultiValidationException.throwInvalid(violations);
     }
 
     private static void validateDebitTransaction(
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionController.java
index 9a3295a2..f90d5276 100644
--- a/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionController.java
+++ b/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionController.java
@@ -5,6 +5,7 @@ import net.hostsharing.hsadminng.context.Context;
 import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeCoopSharesApi;
 import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopSharesTransactionInsertResource;
 import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopSharesTransactionResource;
+import net.hostsharing.hsadminng.errors.MultiValidationException;
 import net.hostsharing.hsadminng.mapper.Mapper;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.format.annotation.DateTimeFormat;
@@ -14,14 +15,12 @@ import org.springframework.transaction.annotation.Transactional;
 import org.springframework.web.bind.annotation.RestController;
 import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
 
-import jakarta.validation.ValidationException;
 import java.time.LocalDate;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.UUID;
 import java.util.function.BiConsumer;
 
-import static java.lang.String.join;
 import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopSharesTransactionTypeResource.CANCELLATION;
 import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopSharesTransactionTypeResource.SUBSCRIPTION;
 
@@ -99,9 +98,7 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
         validateSubscriptionTransaction(requestBody, violations);
         validateCancellationTransaction(requestBody, violations);
         validateshareCount(requestBody, violations);
-        if (violations.size() > 0) {
-            throw new ValidationException("[" + join(", ", violations) + "]");
-        }
+        MultiValidationException.throwInvalid(violations);
     }
 
     private static void validateSubscriptionTransaction(
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/BooleanProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/BooleanProperty.java
new file mode 100644
index 00000000..9d664683
--- /dev/null
+++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/BooleanProperty.java
@@ -0,0 +1,46 @@
+package net.hostsharing.hsadminng.hs.validation;
+
+import lombok.Setter;
+import net.hostsharing.hsadminng.mapper.Array;
+
+import java.util.AbstractMap;
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.Objects;
+
+@Setter
+public class BooleanProperty extends ValidatableProperty<Boolean> {
+
+    private static final String[] KEY_ORDER = Array.join(ValidatableProperty.KEY_ORDER_HEAD, ValidatableProperty.KEY_ORDER_TAIL);
+
+    private Map.Entry<String, String> falseIf;
+
+    private BooleanProperty(final String propertyName) {
+        super(Boolean.class, propertyName, KEY_ORDER);
+    }
+
+    public static BooleanProperty booleanProperty(final String propertyName) {
+        return new BooleanProperty(propertyName);
+    }
+
+    public ValidatableProperty<Boolean> falseIf(final String refPropertyName, final String refPropertyValue) {
+        this.falseIf = new AbstractMap.SimpleImmutableEntry<>(refPropertyName, refPropertyValue);
+        return this;
+    }
+
+    @Override
+    protected void validate(final ArrayList<String> result, final Boolean propValue, final Map<String, Object> props) {
+        if (falseIf != null && propValue) {
+            final Object referencedValue = props.get(falseIf.getKey());
+            if (Objects.equals(referencedValue, falseIf.getValue())) {
+                result.add(propertyName + "' is expected to be false because " +
+                        falseIf.getKey() + "=" + referencedValue + " but is " + propValue);
+            }
+        }
+    }
+
+    @Override
+    protected String simpleTypeName() {
+        return "boolean";
+    }
+}
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/BooleanPropertyValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/BooleanPropertyValidator.java
deleted file mode 100644
index 2838e0f5..00000000
--- a/src/main/java/net/hostsharing/hsadminng/hs/validation/BooleanPropertyValidator.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package net.hostsharing.hsadminng.hs.validation;
-
-import lombok.Setter;
-
-import java.util.AbstractMap;
-import java.util.ArrayList;
-import java.util.Map;
-import java.util.Objects;
-
-@Setter
-public class BooleanPropertyValidator extends HsPropertyValidator<Boolean> {
-
-    private Map.Entry<String, String> falseIf;
-
-    private BooleanPropertyValidator(final String propertyName) {
-        super(Boolean.class, propertyName);
-    }
-
-    public static BooleanPropertyValidator booleanProperty(final String propertyName) {
-        return new BooleanPropertyValidator(propertyName);
-    }
-
-    public HsPropertyValidator<Boolean> falseIf(final String refPropertyName, final String refPropertyValue) {
-        this.falseIf = new AbstractMap.SimpleImmutableEntry<>(refPropertyName, refPropertyValue);
-        return this;
-    }
-
-    @Override
-    protected void validate(final ArrayList<String> result, final String propertiesName, final Boolean propValue, final Map<String, Object> props) {
-        if (falseIf != null && !Objects.equals(props.get(falseIf.getKey()), falseIf.getValue())) {
-            if (propValue) {
-                result.add("'"+propertiesName+"." + propertyName + "' is expected to be false because " +
-                        propertiesName+"." + falseIf.getKey()+ "=" + falseIf.getValue() + " but is " + propValue);
-            }
-        }
-    }
-
-    @Override
-    protected String simpleTypeName() {
-        return "boolean";
-    }
-}
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/EnumerationProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/EnumerationProperty.java
new file mode 100644
index 00000000..23e5ef61
--- /dev/null
+++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/EnumerationProperty.java
@@ -0,0 +1,44 @@
+package net.hostsharing.hsadminng.hs.validation;
+
+import lombok.Setter;
+import net.hostsharing.hsadminng.mapper.Array;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Map;
+
+@Setter
+public class EnumerationProperty extends ValidatableProperty<String> {
+
+    private static final String[] KEY_ORDER = Array.join(
+            ValidatableProperty.KEY_ORDER_HEAD,
+            Array.of("values"),
+            ValidatableProperty.KEY_ORDER_TAIL);
+
+    private String[] values;
+
+    private EnumerationProperty(final String propertyName) {
+        super(String.class, propertyName, KEY_ORDER);
+    }
+
+    public static EnumerationProperty enumerationProperty(final String propertyName) {
+        return new EnumerationProperty(propertyName);
+    }
+
+    public ValidatableProperty<String> values(final String... values) {
+        this.values = values;
+        return this;
+    }
+
+    @Override
+    protected void validate(final ArrayList<String> result, final String propValue, final Map<String, Object> props) {
+        if (Arrays.stream(values).noneMatch(v -> v.equals(propValue))) {
+            result.add(propertyName + "' is expected to be one of " + Arrays.toString(values) + " but is '" + propValue + "'");
+        }
+    }
+
+    @Override
+    protected String simpleTypeName() {
+        return "enumeration";
+    }
+}
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/EnumerationPropertyValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/EnumerationPropertyValidator.java
deleted file mode 100644
index 329feb74..00000000
--- a/src/main/java/net/hostsharing/hsadminng/hs/validation/EnumerationPropertyValidator.java
+++ /dev/null
@@ -1,38 +0,0 @@
-package net.hostsharing.hsadminng.hs.validation;
-
-import lombok.Setter;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Map;
-
-@Setter
-public class EnumerationPropertyValidator extends HsPropertyValidator<String> {
-
-    private String[] values;
-
-    private EnumerationPropertyValidator(final String propertyName) {
-        super(String.class, propertyName);
-    }
-
-    public static EnumerationPropertyValidator enumerationProperty(final String propertyName) {
-        return new EnumerationPropertyValidator(propertyName);
-    }
-
-    public HsPropertyValidator<String> values(final String... values) {
-        this.values = values;
-        return this;
-    }
-
-    @Override
-    protected void validate(final ArrayList<String> result, final String propertiesName, final String propValue, final Map<String, Object> props) {
-        if (Arrays.stream(values).noneMatch(v -> v.equals(propValue))) {
-            result.add("'"+propertiesName+"." + propertyName + "' is expected to be one of " + Arrays.toString(values) + " but is '" + propValue + "'");
-        }
-    }
-
-    @Override
-    protected String simpleTypeName() {
-        return "enumeration";
-    }
-}
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java
index 43be4d10..c06ed140 100644
--- a/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java
+++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java
@@ -1,49 +1,76 @@
 package net.hostsharing.hsadminng.hs.validation;
 
-import com.fasterxml.jackson.annotation.JsonAutoDetect;
-import com.fasterxml.jackson.annotation.PropertyAccessor;
-import com.fasterxml.jackson.databind.ObjectMapper;
 
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
+import java.util.function.Supplier;
 
 import static java.util.Arrays.stream;
+import static java.util.Collections.emptyList;
 
-public class HsEntityValidator<E extends Validatable<E, T>, T extends Enum<T>> {
+public abstract class HsEntityValidator<E> {
 
-    public final HsPropertyValidator<?>[] propertyValidators;
+    public final ValidatableProperty<?>[] propertyValidators;
 
-    public HsEntityValidator(final HsPropertyValidator<?>... validators) {
+    public HsEntityValidator(final ValidatableProperty<?>... validators) {
         propertyValidators = validators;
     }
 
-    public List<String> validate(final E assetEntity) {
+    protected static List<String> enrich(final String prefix, final List<String> messages) {
+        return messages.stream()
+                // TODO:refa: this is a bit hacky, I need to find the right place to add the prefix
+                .map(message -> message.startsWith("'") ? message : ("'" + prefix + "." + message))
+                .toList();
+    }
+
+    protected static String prefix(final String... parts) {
+        return String.join(".", parts);
+    }
+
+    public abstract List<String> validate(final E entity);
+
+    public final List<Map<String, Object>> properties() {
+        return Arrays.stream(propertyValidators)
+                .map(ValidatableProperty::toOrderedMap)
+                .toList();
+    }
+
+    protected ArrayList<String> validateProperties(final Map<String, Object> properties) {
         final var result = new ArrayList<String>();
-        assetEntity.getProperties().keySet().forEach( givenPropName -> {
+        properties.keySet().forEach( givenPropName -> {
             if (stream(propertyValidators).map(pv -> pv.propertyName).noneMatch(propName -> propName.equals(givenPropName))) {
-                result.add("'"+assetEntity.getPropertiesName()+"." + givenPropName + "' is not expected but is set to '" +assetEntity.getProperties().get(givenPropName) + "'");
+                result.add(givenPropName + "' is not expected but is set to '" + properties.get(givenPropName) + "'");
             }
         });
         stream(propertyValidators).forEach(pv -> {
-          result.addAll(pv.validate(assetEntity.getPropertiesName(), assetEntity.getProperties()));
+            result.addAll(pv.validate(properties));
         });
         return result;
     }
 
-    public List<Map<String, Object>> properties() {
-        final var mapper = new ObjectMapper();
-        mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
-        return Arrays.stream(propertyValidators)
-                .map(propertyValidator -> propertyValidator.toMap(mapper))
-                .map(HsEntityValidator::asKeyValueMap)
-                .toList();
+    @SafeVarargs
+    protected static List<String> sequentiallyValidate(final Supplier<List<String>>... validators) {
+        return new ArrayList<>(stream(validators)
+                .map(Supplier::get)
+                .filter(violations -> !violations.isEmpty())
+                .findFirst()
+                .orElse(emptyList()));
     }
 
-    @SuppressWarnings({ "unchecked", "rawtypes" })
-    private static Map<String, Object> asKeyValueMap(final Map map) {
-        return (Map<String, Object>) map;
+    protected static Integer getNonNullIntegerValue(final ValidatableProperty<?> prop, final Map<String, Object> propValues) {
+        final var value = prop.getValue(propValues);
+        if (value instanceof Integer) {
+            return (Integer) value;
+        }
+        throw new IllegalArgumentException(prop.propertyName + " Integer value expected, but got " + value);
     }
 
+    protected static Integer toNonNullInteger(final Object value) {
+        if (value instanceof Integer) {
+            return (Integer) value;
+        }
+        throw new IllegalArgumentException("Integer value expected, but got " + value);
+    }
 }
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/HsPropertyValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/HsPropertyValidator.java
deleted file mode 100644
index 891c8a7a..00000000
--- a/src/main/java/net/hostsharing/hsadminng/hs/validation/HsPropertyValidator.java
+++ /dev/null
@@ -1,67 +0,0 @@
-package net.hostsharing.hsadminng.hs.validation;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
-import lombok.RequiredArgsConstructor;
-
-import java.util.AbstractMap.SimpleImmutableEntry;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-
-@RequiredArgsConstructor
-public abstract class HsPropertyValidator<T> {
-
-    final Class<T> type;
-    final String propertyName;
-    private Boolean required;
-
-    public static <K, V> Map.Entry<K, V> defType(K k, V v) {
-        return new SimpleImmutableEntry<>(k, v);
-    }
-
-    public HsPropertyValidator<T> required() {
-        required = Boolean.TRUE;
-        return this;
-    }
-
-    public HsPropertyValidator<T> optional() {
-        required = Boolean.FALSE;
-        return this;
-    }
-
-    public final List<String> validate(final String propertiesName, final Map<String, Object> props) {
-        final var result = new ArrayList<String>();
-        final var propValue = props.get(propertyName);
-        if (propValue == null) {
-            if (required) {
-                result.add("'"+propertiesName+"." + propertyName + "' is required but missing");
-            }
-        }
-        if (propValue != null){
-            if ( type.isInstance(propValue)) {
-                //noinspection unchecked
-                validate(result, propertiesName, (T) propValue, props);
-            } else {
-                result.add("'"+propertiesName+"." + propertyName + "' is expected to be of type " + type + ", " +
-                        "but is of type '" + propValue.getClass().getSimpleName() + "'");
-            }
-        }
-        return result;
-    }
-
-    protected abstract void validate(final ArrayList<String> result, final String propertiesName, final T propValue, final Map<String, Object> props);
-
-    public void verifyConsistency(final Map.Entry<? extends Enum<?>, ?> typeDef) {
-        if (required == null ) {
-            throw new IllegalStateException(typeDef.getKey() + "[" + propertyName + "] not fully initialized, please call either .required() or .optional()" );
-        }
-    }
-
-    public Map<String, Object> toMap(final ObjectMapper mapper) {
-        final Map<String, Object> map = mapper.convertValue(this, Map.class);
-        map.put("type", simpleTypeName());
-        return map;
-    }
-
-    protected abstract String simpleTypeName();
-}
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/IntegerProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/IntegerProperty.java
new file mode 100644
index 00000000..a1658ff9
--- /dev/null
+++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/IntegerProperty.java
@@ -0,0 +1,56 @@
+package net.hostsharing.hsadminng.hs.validation;
+
+import lombok.Setter;
+import net.hostsharing.hsadminng.mapper.Array;
+
+import java.util.ArrayList;
+import java.util.Map;
+
+@Setter
+public class IntegerProperty extends ValidatableProperty<Integer> {
+
+    private final static String[] KEY_ORDER = Array.join(
+            ValidatableProperty.KEY_ORDER_HEAD,
+            Array.of("unit", "min", "max", "step"),
+            ValidatableProperty.KEY_ORDER_TAIL);
+
+    private String unit;
+    private Integer min;
+    private Integer max;
+    private Integer step;
+
+    public static IntegerProperty integerProperty(final String propertyName) {
+        return new IntegerProperty(propertyName);
+    }
+
+    private IntegerProperty(final String propertyName) {
+        super(Integer.class, propertyName, KEY_ORDER);
+    }
+
+    @Override
+    public String unit() {
+        return unit;
+    }
+
+    public Integer max() {
+        return max;
+    }
+
+    @Override
+    protected void validate(final ArrayList<String> result, final Integer propValue, final Map<String, Object> props) {
+        if (min != null && propValue < min) {
+            result.add(propertyName + "' is expected to be >= " + min + " but is " + propValue);
+        }
+        if (max != null && propValue > max) {
+            result.add(propertyName + "' is expected to be <= " + max + " but is " + propValue);
+        }
+        if (step != null && propValue % step != 0) {
+            result.add(propertyName + "' is expected to be multiple of " + step + " but is " + propValue);
+        }
+    }
+
+    @Override
+    protected String simpleTypeName() {
+        return "integer";
+    }
+}
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/IntegerPropertyValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/IntegerPropertyValidator.java
deleted file mode 100644
index d6fb85f5..00000000
--- a/src/main/java/net/hostsharing/hsadminng/hs/validation/IntegerPropertyValidator.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package net.hostsharing.hsadminng.hs.validation;
-
-import lombok.Setter;
-
-import java.util.ArrayList;
-import java.util.Map;
-
-@Setter
-public class IntegerPropertyValidator extends HsPropertyValidator<Integer> {
-
-    private String unit;
-    private Integer min;
-    private Integer max;
-    private Integer step;
-
-    public static IntegerPropertyValidator integerProperty(final String propertyName) {
-        return new IntegerPropertyValidator(propertyName);
-    }
-
-    private IntegerPropertyValidator(final String propertyName) {
-        super(Integer.class, propertyName);
-    }
-
-
-    @Override
-    protected void validate(final ArrayList<String> result, final String propertiesName, final Integer propValue, final Map<String, Object> props) {
-        if (min != null && propValue < min) {
-            result.add("'"+propertiesName+"." + propertyName + "' is expected to be >= " + min + " but is " + propValue);
-        }
-        if (max != null && propValue > max) {
-            result.add("'"+propertiesName+"." + propertyName + "' is expected to be <= " + max + " but is " + propValue);
-        }
-        if (step != null && propValue % step != 0) {
-            result.add("'"+propertiesName+"." + propertyName + "' is expected to be multiple of " + step + " but is " + propValue);
-        }
-    }
-
-    @Override
-    protected String simpleTypeName() {
-        return "integer";
-    }
-}
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/Validatable.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/Validatable.java
deleted file mode 100644
index 6f214b04..00000000
--- a/src/main/java/net/hostsharing/hsadminng/hs/validation/Validatable.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package net.hostsharing.hsadminng.hs.validation;
-
-
-import java.util.Map;
-
-public interface Validatable<E, T extends Enum<T>> {
-
-
-    Enum<T> getType();
-
-    String getPropertiesName();
-    Map<String, Object> getProperties();
-}
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java
new file mode 100644
index 00000000..7795d47d
--- /dev/null
+++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java
@@ -0,0 +1,172 @@
+package net.hostsharing.hsadminng.hs.validation;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import lombok.RequiredArgsConstructor;
+import lombok.SneakyThrows;
+import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
+import net.hostsharing.hsadminng.mapper.Array;
+import org.apache.commons.lang3.function.TriFunction;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.Function;
+
+import static java.lang.Boolean.FALSE;
+import static java.lang.Boolean.TRUE;
+import static java.util.Collections.emptyList;
+
+@RequiredArgsConstructor
+public abstract class ValidatableProperty<T> {
+
+    protected static final String[] KEY_ORDER_HEAD = Array.of("propertyName");
+    protected static final String[] KEY_ORDER_TAIL = Array.of("required", "defaultValue", "isTotalsValidator", "thresholdPercentage");
+
+    final Class<T> type;
+    final String propertyName;
+    private final String[] keyOrder;
+    private Boolean required;
+    private T defaultValue;
+    private boolean isTotalsValidator = false;
+    @JsonIgnore
+    private List<Function<HsBookingItemEntity, List<String>>> asTotalLimitValidators; // TODO.impl: move to BookingItemIntegerProperty
+
+    private Integer thresholdPercentage; // TODO.impl: move to IntegerProperty
+
+    public String unit() {
+        return null;
+    }
+
+    public ValidatableProperty<T> required() {
+        required = TRUE;
+        return this;
+    }
+
+    public ValidatableProperty<T> optional() {
+        required = FALSE;
+        return this;
+    }
+
+    public ValidatableProperty<T> withDefault(final T value) {
+        defaultValue = value;
+        required = FALSE;
+        return this;
+    }
+
+    public ValidatableProperty<T> asTotalLimit() {
+        isTotalsValidator = true;
+        return this;
+    }
+
+    public String propertyName() {
+        return propertyName;
+    }
+
+    public boolean isTotalsValidator() {
+        return isTotalsValidator || asTotalLimitValidators != null;
+    }
+
+    public Integer thresholdPercentage() {
+        return thresholdPercentage;
+    }
+
+    public ValidatableProperty<T> eachComprising(final int factor, final TriFunction<HsBookingItemEntity, IntegerProperty, Integer, List<String>> validator) {
+        if (asTotalLimitValidators == null) {
+            asTotalLimitValidators = new ArrayList<>();
+        }
+        asTotalLimitValidators.add((final HsBookingItemEntity entity) -> validator.apply(entity, (IntegerProperty)this,  factor));
+        return this;
+    }
+
+    public ValidatableProperty<?> withThreshold(final Integer percentage) {
+        this.thresholdPercentage = percentage;
+        return this;
+    }
+
+    public final List<String> validate(final Map<String, Object> props) {
+        final var result = new ArrayList<String>();
+        final var propValue = props.get(propertyName);
+        if (propValue == null) {
+            if (required) {
+                result.add(propertyName + "' is required but missing");
+            }
+        }
+        if (propValue != null){
+            if ( type.isInstance(propValue)) {
+                //noinspection unchecked
+                validate(result, (T) propValue, props);
+            } else {
+                result.add(propertyName + "' is expected to be of type " + type + ", " +
+                        "but is of type '" + propValue.getClass().getSimpleName() + "'");
+            }
+        }
+        return result;
+    }
+
+    protected abstract void validate(final ArrayList<String> result, final T propValue, final Map<String, Object> props);
+
+    public void verifyConsistency(final Map.Entry<? extends Enum<?>, ?> typeDef) {
+        if (required == null ) {
+            throw new IllegalStateException(typeDef.getKey() + "[" + propertyName + "] not fully initialized, please call either .required() or .optional()" );
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    public T getValue(final Map<String, Object> propValues) {
+        return (T) Optional.ofNullable(propValues.get(propertyName)).orElse(defaultValue);
+    }
+
+    protected abstract String simpleTypeName();
+
+    public Map<String, Object> toOrderedMap() {
+            Map<String, Object> sortedMap = new LinkedHashMap<>();
+            sortedMap.put("type", simpleTypeName());
+
+            // Add entries according to the given order
+            for (String key : keyOrder) {
+                final Optional<Object> propValue = getPropertyValue(key);
+                propValue.ifPresent(o -> sortedMap.put(key, o));
+            }
+
+            return sortedMap;
+    }
+
+    @SneakyThrows
+    private Optional<Object> getPropertyValue(final String key) {
+        try {
+            final var field = getClass().getDeclaredField(key);
+            field.setAccessible(true);
+            return Optional.ofNullable(arrayToList(field.get(this)));
+        } catch (final NoSuchFieldException e1) {
+            try {
+                final var field = getClass().getSuperclass().getDeclaredField(key);
+                field.setAccessible(true);
+                return Optional.ofNullable(arrayToList(field.get(this)));
+            } catch (final NoSuchFieldException e2) {
+                return Optional.empty();
+            }
+        }
+    }
+
+    private Object arrayToList(final Object value) {
+        if ( value instanceof String[]) {
+            return List.of((String[])value);
+        }
+        return value;
+    }
+
+    public List<String> validateTotals(final HsBookingItemEntity bookingItem) {
+        if (asTotalLimitValidators==null) {
+            return emptyList();
+        }
+        return asTotalLimitValidators.stream()
+                .map(v -> v.apply(bookingItem))
+                .filter(Objects::nonNull)
+                .flatMap(Collection::stream)
+                .toList();
+    }
+}
diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/test/Array.java b/src/main/java/net/hostsharing/hsadminng/mapper/Array.java
similarity index 83%
rename from src/test/java/net/hostsharing/hsadminng/rbac/test/Array.java
rename to src/main/java/net/hostsharing/hsadminng/mapper/Array.java
index c51a69bb..39588f11 100644
--- a/src/test/java/net/hostsharing/hsadminng/rbac/test/Array.java
+++ b/src/main/java/net/hostsharing/hsadminng/mapper/Array.java
@@ -1,4 +1,4 @@
-package net.hostsharing.hsadminng.rbac.test;
+package net.hostsharing.hsadminng.mapper;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -37,4 +37,10 @@ public class Array {
         return resultList.toArray(String[]::new);
     }
 
+    public static String[] join(final String[]... parts) {
+        final String[] joined =  Arrays.stream(parts)
+                .flatMap(Arrays::stream)
+                .toArray(String[]::new);
+        return joined;
+    }
 }
diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantsDiagramService.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantsDiagramService.java
index 2290c948..fd33f358 100644
--- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantsDiagramService.java
+++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantsDiagramService.java
@@ -62,6 +62,8 @@ public class RbacGrantsDiagramService {
     @PersistenceContext
     private EntityManager em;
 
+    private Map<UUID, List<RawRbacGrantEntity>> descendantsByUuid = new HashMap<>();
+
     public String allGrantsToCurrentUser(final EnumSet<Include> includes) {
         final var graph = new LimitedHashSet<RawRbacGrantEntity>();
         for ( UUID subjectUuid: context.currentSubjectsUuids() ) {
@@ -102,7 +104,7 @@ public class RbacGrantsDiagramService {
     }
 
     private void traverseGrantsFrom(final Set<RawRbacGrantEntity> graph, final UUID refUuid, final EnumSet<Include> option) {
-        final var grants = rawGrantRepo.findByDescendantUuid(refUuid);
+        final var grants = findDescendantsByUuid(refUuid);
         grants.forEach(g -> {
             if (!option.contains(USERS) && g.getAscendantIdName().startsWith("user:")) {
                 return;
@@ -114,6 +116,11 @@ public class RbacGrantsDiagramService {
         });
     }
 
+    private List<RawRbacGrantEntity> findDescendantsByUuid(final UUID refUuid) {
+        // TODO.impl: if that UUID already got processed, do we need to return anything at all?
+        return descendantsByUuid.computeIfAbsent(refUuid, uuid -> rawGrantRepo.findByDescendantUuid(uuid));
+    }
+
     private String toMermaidFlowchart(final HashSet<RawRbacGrantEntity> graph, final EnumSet<Include> includes) {
         final var entities =
                 includes.contains(DETAILS)
diff --git a/src/main/resources/db/changelog/6-hs-booking/630-booking-item/6208-hs-booking-item-test-data.sql b/src/main/resources/db/changelog/6-hs-booking/630-booking-item/6208-hs-booking-item-test-data.sql
index bc3a9e51..3f007ab8 100644
--- a/src/main/resources/db/changelog/6-hs-booking/630-booking-item/6208-hs-booking-item-test-data.sql
+++ b/src/main/resources/db/changelog/6-hs-booking/630-booking-item/6208-hs-booking-item-test-data.sql
@@ -33,13 +33,13 @@ begin
     managedServerUuid := uuid_generate_v4();
     insert
         into hs_booking_item (uuid, projectuuid,            type,               parentitemuuid,     caption,                    validity,                           resources)
-        values (privateCloudUuid,   relatedProject.uuid,    'PRIVATE_CLOUD',    null,               'some PrivateCloud',        daterange('20240401', null, '[]'),  '{ "CPUs": 10, "SDD": 10240, "HDD": 10240, "Traffic": 42 }'::jsonb),
-               (uuid_generate_v4(), null,                   'MANAGED_SERVER',   privateCloudUuid,   'some ManagedServer',       daterange('20230115', '20240415',   '[)'), '{ "CPUs": 2, "RAM": 4, "HDD": 1024, "Traffic": 42 }'::jsonb),
-               (uuid_generate_v4(), null,                   'CLOUD_SERVER',     privateCloudUuid,   'test CloudServer',         daterange('20230115', '20240415',   '[)'), '{ "CPUs": 2, "RAM": 4, "HDD": 1024, "Traffic": 42 }'::jsonb),
-               (uuid_generate_v4(), null,                   'CLOUD_SERVER',     privateCloudUuid,   'prod CloudServer',         daterange('20230115', '20240415',   '[)'), '{ "CPUs": 4, "RAM": 16, "HDD": 2924, "Traffic": 420 }'::jsonb),
-               (managedServerUuid,  relatedProject.uuid,    'MANAGED_SERVER',   null,               'separate ManagedServer',   daterange('20221001', null, '[]'),  '{ "CPUs": 2, "RAM": 8, "SDD": 512, "Traffic": 42 }'::jsonb),
-               (uuid_generate_v4(), null,                   'MANAGED_WEBSPACE', managedServerUuid,  'some ManagedWebspace',     daterange('20221001', null, '[]'),  '{ "SDD": 512, "Traffic": 12, "Daemons": 2, "Multi": 4 }'::jsonb),
-               (uuid_generate_v4(), relatedProject.uuid,    'MANAGED_WEBSPACE', null,               'some ManagedWebspace',     daterange('20221001', null, '[]'),  '{ "SDD": 512, "Traffic": 12, "Daemons": 2, "Multi": 4 }'::jsonb);
+        values (privateCloudUuid,   relatedProject.uuid,    'PRIVATE_CLOUD',    null,               'some PrivateCloud',        daterange('20240401', null, '[]'),  '{ "CPUs": 10, "RAM": 32, "SSD": 4000, "HDD": 10000, "Traffic": 2000 }'::jsonb),
+               (uuid_generate_v4(), null,                   'MANAGED_SERVER',   privateCloudUuid,   'some ManagedServer',       daterange('20230115', '20240415',   '[)'), '{ "CPUs": 2, "RAM": 4, "SSD": 500, "Traffic": 500 }'::jsonb),
+               (uuid_generate_v4(), null,                   'CLOUD_SERVER',     privateCloudUuid,   'test CloudServer',         daterange('20230115', '20240415',   '[)'), '{ "CPUs": 2, "RAM": 4, "SSD": 750, "Traffic": 500 }'::jsonb),
+               (uuid_generate_v4(), null,                   'CLOUD_SERVER',     privateCloudUuid,   'prod CloudServer',         daterange('20230115', '20240415',   '[)'), '{ "CPUs": 4, "RAM": 16, "SSD": 1000, "Traffic": 500 }'::jsonb),
+               (managedServerUuid,  relatedProject.uuid,    'MANAGED_SERVER',   null,               'separate ManagedServer',   daterange('20221001', null, '[]'),  '{ "CPUs": 2, "RAM": 8, "SSD": 500, "Traffic": 500 }'::jsonb),
+               (uuid_generate_v4(), null,                   'MANAGED_WEBSPACE', managedServerUuid,  'some ManagedWebspace',     daterange('20221001', null, '[]'),  '{ "SSD": 50, "Traffic": 20, "Daemons": 2, "Multi": 4 }'::jsonb),
+               (uuid_generate_v4(), relatedProject.uuid,    'MANAGED_WEBSPACE', null,               'separate ManagedWebspace', daterange('20221001', null, '[]'),  '{ "SSD": 100, "Traffic": 50, "Daemons": 0, "Multi": 1 }'::jsonb);
 end; $$;
 --//
 
diff --git a/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7010-hs-hosting-asset.sql b/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7010-hs-hosting-asset.sql
index c6fedb72..7e96a3fd 100644
--- a/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7010-hs-hosting-asset.sql
+++ b/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7010-hs-hosting-asset.sql
@@ -75,10 +75,10 @@ begin
     end);
 
     if expectedParentType is not null and actualParentType is null then
-        raise exception '[400] % must have % as parent, but got <NULL>',
+        raise exception '[400] HostingAsset % must have % as parent, but got <NULL>',
             NEW.type, expectedParentType;
     elsif expectedParentType is not null and actualParentType <> expectedParentType then
-        raise exception '[400] % must have % as parent, but got %s',
+        raise exception '[400] HostingAsset % must have % as parent, but got %s',
             NEW.type, expectedParentType, actualParentType;
     end if;
     return NEW;
@@ -100,27 +100,23 @@ create or replace function hs_hosting_asset_booking_item_hierarchy_check_tf()
     language plpgsql as $$
 declare
     actualBookingItemType       HsBookingItemType;
-    expectedBookingItemTypes    HsBookingItemType[];
+    expectedBookingItemType     HsBookingItemType;
 begin
     actualBookingItemType := (select type
                                  from hs_booking_item
                                  where NEW.bookingItemUuid = uuid);
 
     if NEW.type = 'CLOUD_SERVER' then
-        expectedBookingItemTypes := ARRAY['PRIVATE_CLOUD', 'CLOUD_SERVER'];
+        expectedBookingItemType := 'CLOUD_SERVER';
     elsif NEW.type = 'MANAGED_SERVER' then
-        expectedBookingItemTypes := ARRAY['PRIVATE_CLOUD', 'MANAGED_SERVER'];
+        expectedBookingItemType := 'MANAGED_SERVER';
     elsif NEW.type = 'MANAGED_WEBSPACE' then
-        if NEW.parentAssetUuid is null then
-            expectedBookingItemTypes := ARRAY['MANAGED_WEBSPACE'];
-        else
-            expectedBookingItemTypes := ARRAY['PRIVATE_CLOUD', 'MANAGED_SERVER'];
-        end if;
+        expectedBookingItemType := 'MANAGED_WEBSPACE';
     end if;
 
-    if not actualBookingItemType = any(expectedBookingItemTypes) then
-        raise exception '[400] % % must have any of % as booking-item, but got %',
-            NEW.type, NEW.identifier, expectedBookingItemTypes, actualBookingItemType;
+    if not actualBookingItemType = expectedBookingItemType then
+        raise exception '[400] HostingAsset % % must have % as booking-item, but got %',
+            NEW.type, NEW.identifier, expectedBookingItemType, actualBookingItemType;
     end if;
     return NEW;
 end; $$;
diff --git a/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7018-hs-hosting-asset-test-data.sql b/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7018-hs-hosting-asset-test-data.sql
index 964acdec..c82bd768 100644
--- a/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7018-hs-hosting-asset-test-data.sql
+++ b/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7018-hs-hosting-asset-test-data.sql
@@ -11,16 +11,18 @@
 create or replace procedure createHsHostingAssetTestData(givenProjectCaption varchar)
     language plpgsql as $$
 declare
-    currentTask                     varchar;
-    relatedProject                  hs_booking_project;
-    relatedDebitor                  hs_office_debitor;
-    relatedPrivateCloudBookingItem  hs_booking_item;
-    relatedManagedServerBookingItem hs_booking_item;
-    debitorNumberSuffix             varchar;
-    defaultPrefix                   varchar;
-    managedServerUuid               uuid;
-    managedWebspaceUuid             uuid;
-    webUnixUserUuid                 uuid;
+    currentTask                         varchar;
+    relatedProject                      hs_booking_project;
+    relatedDebitor                      hs_office_debitor;
+    relatedPrivateCloudBookingItem      hs_booking_item;
+    relatedManagedServerBookingItem     hs_booking_item;
+    relatedCloudServerBookingItem       hs_booking_item;
+    relatedManagedWebspaceBookingItem   hs_booking_item;
+    debitorNumberSuffix                 varchar;
+    defaultPrefix                       varchar;
+    managedServerUuid                   uuid;
+    managedWebspaceUuid                 uuid;
+    webUnixUserUuid                     uuid;
 begin
     currentTask := 'creating hosting-asset test-data ' || givenProjectCaption;
     call defineContext(currentTask, null, 'superuser-alex@hostsharing.net', 'global#global:ADMIN');
@@ -38,7 +40,7 @@ begin
 
     select item.* into relatedPrivateCloudBookingItem
         from hs_booking_item item
-        where item.projectUuid = relatedProject.uuid
+       where item.projectUuid = relatedProject.uuid
           and item.type = 'PRIVATE_CLOUD';
     assert relatedPrivateCloudBookingItem.uuid is not null, 'relatedPrivateCloudBookingItem for "' || givenProjectCaption|| '" must not be null';
 
@@ -48,6 +50,18 @@ begin
             and item.type = 'MANAGED_SERVER';
     assert relatedManagedServerBookingItem.uuid is not null, 'relatedManagedServerBookingItem for "' || givenProjectCaption|| '" must not be null';
 
+    select item.* into relatedCloudServerBookingItem
+          from hs_booking_item item
+          where item.parentItemuuid = relatedPrivateCloudBookingItem.uuid
+            and item.type = 'CLOUD_SERVER';
+    assert relatedCloudServerBookingItem.uuid is not null, 'relatedCloudServerBookingItem for "' || givenProjectCaption|| '" must not be null';
+
+    select item.* into relatedManagedWebspaceBookingItem
+          from hs_booking_item item
+          where item.projectUuid = relatedProject.uuid
+            and item.type = 'MANAGED_WEBSPACE';
+    assert relatedManagedWebspaceBookingItem.uuid is not null, 'relatedManagedWebspaceBookingItem for "' || givenProjectCaption|| '" must not be null';
+
     select uuid_generate_v4() into managedServerUuid;
     select uuid_generate_v4() into managedWebspaceUuid;
     select uuid_generate_v4() into webUnixUserUuid;
@@ -55,12 +69,12 @@ begin
     defaultPrefix := relatedDebitor.defaultPrefix;
 
     insert into hs_hosting_asset
-           (uuid,                bookingitemuuid,                      type,                parentAssetUuid,     assignedToAssetUuid,   identifier,                      caption,                     config)
-    values (managedServerUuid,   relatedPrivateCloudBookingItem.uuid,  'MANAGED_SERVER',    null,                null,                  'vm10' || debitorNumberSuffix,   'some ManagedServer',        '{ "extra": 42 }'::jsonb),
-           (uuid_generate_v4(),  relatedPrivateCloudBookingItem.uuid,  'CLOUD_SERVER',      null,                null,                  'vm20' || debitorNumberSuffix,   'another CloudServer',       '{ "extra": 42 }'::jsonb),
-           (managedWebspaceUuid, relatedManagedServerBookingItem.uuid, 'MANAGED_WEBSPACE',  managedServerUuid,   null,                  defaultPrefix || '01',           'some Webspace',             '{ "extra": 42 }'::jsonb),
-           (webUnixUserUuid,     null,                                 'UNIX_USER',         managedWebspaceUuid, null,                  defaultPrefix || '01-web',       'some UnixUser for Website', '{ "SSD-soft-quota": "128", "SSD-hard-quota": "256", "HDD-soft-quota": "512", "HDD-hard-quota": "1024", "extra": 42 }'::jsonb),
-           (uuid_generate_v4(),  null,                                 'DOMAIN_HTTP_SETUP', managedWebspaceUuid, webUnixUserUuid,       defaultPrefix || '.example.org', 'some Domain-HTTP-Setup',    '{ "option-htdocsfallback": true, "use-fcgiphpbin": "/usr/lib/cgi-bin/php", "validsubdomainnames": "*", "extra": 42 }'::jsonb);
+           (uuid,                bookingitemuuid,                        type,                parentAssetUuid,     assignedToAssetUuid,   identifier,                      caption,                     config)
+    values (managedServerUuid,   relatedManagedServerBookingItem.uuid,   'MANAGED_SERVER',    null,                null,                  'vm10' || debitorNumberSuffix,   'some ManagedServer',        '{ "monit_max_cpu_usage": 90, "monit_max_ram_usage": 80, "monit_max_ssd_usage": 70 }'::jsonb),
+           (uuid_generate_v4(),  relatedCloudServerBookingItem.uuid,     'CLOUD_SERVER',      null,                null,                  'vm20' || debitorNumberSuffix,   'another CloudServer',       '{}'::jsonb),
+           (managedWebspaceUuid, relatedManagedWebspaceBookingItem.uuid, 'MANAGED_WEBSPACE',  managedServerUuid,   null,                  defaultPrefix || '01',           'some Webspace',             '{}'::jsonb),
+           (webUnixUserUuid,     null,                                   'UNIX_USER',         managedWebspaceUuid, null,                  defaultPrefix || '01-web',       'some UnixUser for Website', '{ "SSD-soft-quota": "128", "SSD-hard-quota": "256", "HDD-soft-quota": "512", "HDD-hard-quota": "1024"}'::jsonb),
+           (uuid_generate_v4(),  null,                                   'DOMAIN_HTTP_SETUP', managedWebspaceUuid, webUnixUserUuid,       defaultPrefix || '.example.org', 'some Domain-HTTP-Setup',    '{ "option-htdocsfallback": true, "use-fcgiphpbin": "/usr/lib/cgi-bin/php", "validsubdomainnames": "*"}'::jsonb);
 end; $$;
 --//
 
diff --git a/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java b/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java
index 2c2f9f3d..df26279d 100644
--- a/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java
+++ b/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java
@@ -8,7 +8,10 @@ import com.tngtech.archunit.lang.ArchRule;
 import com.tngtech.archunit.lang.ConditionEvents;
 import com.tngtech.archunit.lang.SimpleConditionEvent;
 import net.hostsharing.hsadminng.HsadminNgApplication;
+import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
+import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
 import net.hostsharing.hsadminng.rbac.context.ContextBasedTest;
+import net.hostsharing.hsadminng.rbac.rbacgrant.RbacGrantsDiagramService;
 import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
 import org.springframework.data.repository.Repository;
 import org.springframework.web.bind.annotation.RestController;
@@ -51,6 +54,7 @@ public class ArchitectureTest {
                     "..hs.office.person",
                     "..hs.office.relation",
                     "..hs.office.sepamandate",
+                    "..hs.booking.debitor",
                     "..hs.booking.project",
                     "..hs.booking.item",
                     "..hs.booking.item.validators",
@@ -155,7 +159,8 @@ public class ArchitectureTest {
             .that().resideInAPackage("..hs.hosting.(*)..")
             .should().onlyBeAccessed().byClassesThat()
             .resideInAnyPackage(
-                    "..hs.hosting.(*).."
+                    "..hs.hosting.(*)..",
+                    "..hs.booking.(*).." // TODO.impl: fix this cyclic dependency
             );
 
     @ArchTest
@@ -295,9 +300,13 @@ public class ArchitectureTest {
     static final ArchRule everythingShouldBeFreeOfCycles =
         slices().matching("net.hostsharing.hsadminng.(*)..")
                 .should().beFreeOfCycles()
+                // TODO.refa: would be great if we could get rid of these cyclic dependencies
                 .ignoreDependency(
                         ContextBasedTest.class,
-                        net.hostsharing.hsadminng.rbac.rbacgrant.RbacGrantsDiagramService.class);
+                        RbacGrantsDiagramService.class)
+                .ignoreDependency(
+                        HsBookingItemEntity.class,
+                        HsHostingAssetEntity.class);
 
 
     @ArchTest
diff --git a/src/test/java/net/hostsharing/hsadminng/errors/RestResponseEntityExceptionHandlerUnitTest.java b/src/test/java/net/hostsharing/hsadminng/errors/RestResponseEntityExceptionHandlerUnitTest.java
index ad3cdfa0..9b25fed4 100644
--- a/src/test/java/net/hostsharing/hsadminng/errors/RestResponseEntityExceptionHandlerUnitTest.java
+++ b/src/test/java/net/hostsharing/hsadminng/errors/RestResponseEntityExceptionHandlerUnitTest.java
@@ -187,7 +187,7 @@ class RestResponseEntityExceptionHandlerUnitTest {
         final var givenWebRequest = mock(WebRequest.class);
 
         // when
-        final var errorResponse = exceptionHandler.handleIbanAndBicExceptions(givenException, givenWebRequest);
+        final var errorResponse = exceptionHandler.handleValidationExceptions(givenException, givenWebRequest);
 
         // then
         assertThat(errorResponse.getBody().getStatusCode()).isEqualTo(400);
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/debitor/HsBookingDebitorEntityTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/debitor/HsBookingDebitorEntityUnitTest.java
similarity index 95%
rename from src/test/java/net/hostsharing/hsadminng/hs/booking/debitor/HsBookingDebitorEntityTest.java
rename to src/test/java/net/hostsharing/hsadminng/hs/booking/debitor/HsBookingDebitorEntityUnitTest.java
index 4275c56c..154e2b89 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/booking/debitor/HsBookingDebitorEntityTest.java
+++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/debitor/HsBookingDebitorEntityUnitTest.java
@@ -4,7 +4,7 @@ import org.junit.jupiter.api.Test;
 
 import static org.assertj.core.api.Assertions.assertThat;
 
-class HsBookingDebitorEntityTest {
+class HsBookingDebitorEntityUnitTest {
 
     @Test
     void toStringContainsDebitorNumberAndDefaultPrefix() {
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java
index a0054b4f..2804a758 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java
+++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java
@@ -77,14 +77,14 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup
                     [
                         {
                             "type": "MANAGED_WEBSPACE",
-                            "caption": "some ManagedWebspace",
+                            "caption": "separate ManagedWebspace",
                             "validFrom": "2022-10-01",
                             "validTo": null,
                             "resources": {
-                                "SDD": 512,
-                                "Multi": 4,
-                                "Daemons": 2,
-                                "Traffic": 12
+                                "SSD": 100,
+                                "Multi": 1,
+                                "Daemons": 0,
+                                "Traffic": 50
                             }
                         },
                         {
@@ -94,9 +94,9 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup
                             "validTo": null,
                             "resources": {
                                 "RAM": 8,
-                                "SDD": 512,
+                                "SSD": 500,
                                 "CPUs": 2,
-                                "Traffic": 42
+                                "Traffic": 500
                             }
                         },
                         {
@@ -105,10 +105,11 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup
                             "validFrom": "2024-04-01",
                             "validTo": null,
                             "resources": {
-                                "HDD": 10240,
-                                "SDD": 10240,
+                                "HDD": 10000,
+                                "RAM": 32,
+                                "SSD": 4000,
                                 "CPUs": 10,
-                                "Traffic": 42
+                                "Traffic": 2000
                             }
                         }
                     ]
@@ -174,7 +175,7 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup
         @Test
         void globalAdmin_canGetArbitraryBookingItem() {
             context.define("superuser-alex@hostsharing.net");
-            final var givenBookingItemUuid = bookingItemRepo.findByCaption("some ManagedWebspace").stream()
+            final var givenBookingItemUuid = bookingItemRepo.findByCaption("separate ManagedWebspace").stream()
                             .filter(bi -> belongsToDebitorWithDefaultPrefix(bi, "fir"))
                             .map(HsBookingItemEntity::getUuid)
                             .findAny().orElseThrow();
@@ -191,14 +192,14 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup
                     .body("", lenientlyEquals("""
                         {
                             "type": "MANAGED_WEBSPACE",
-                             "caption": "some ManagedWebspace",
+                             "caption": "separate ManagedWebspace",
                              "validFrom": "2022-10-01",
                              "validTo": null,
                              "resources": {
-                                 "SDD": 512,
-                                 "Multi": 4,
-                                 "Daemons": 2,
-                                 "Traffic": 12
+                                 "SSD": 100,
+                                 "Multi": 1,
+                                 "Daemons": 0,
+                                 "Traffic": 50
                             }
                         }
                     """)); // @formatter:on
@@ -227,14 +228,16 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup
         void projectAdmin_canGetRelatedBookingItem() {
             context.define("superuser-alex@hostsharing.net");
             final var givenBookingItemUuid = bookingItemRepo.findByCaption("separate ManagedServer").stream()
-                    .filter(bi -> belongsToDebitorWithDefaultPrefix(bi, "thi"))
+                    .filter(bi -> belongsToDebitorWithDefaultPrefix(bi, "sec"))
                     .map(HsBookingItemEntity::getUuid)
                     .findAny().orElseThrow();
 
+            generateRbacDiagramForObjectPermission(givenBookingItemUuid, "SELECT", "select");
+
             RestAssured // @formatter:off
                 .given()
                     .header("current-user", "superuser-alex@hostsharing.net")
-                    .header("assumed-roles", "hs_booking_project#D-1000313-D-1000313defaultproject:ADMIN")
+                    .header("assumed-roles", "hs_booking_project#D-1000212-D-1000212defaultproject:ADMIN")
                     .port(port)
                 .when()
                     .get("http://localhost/api/hs/booking/items/" + givenBookingItemUuid)
@@ -249,9 +252,9 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup
                             "validTo": null,
                             "resources": {
                                 "RAM": 8,
-                                "SDD": 512,
+                                "SSD": 500,
                                 "CPUs": 2,
-                                "Traffic": 42
+                                "Traffic": 500
                             }
                         }
                     """)); // @formatter:on
@@ -261,7 +264,7 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup
             return ofNullable(bi)
                     .map(HsBookingItemEntity::getProject)
                     .map(HsBookingProjectEntity::getDebitor)
-                    .map(bd -> bd.getDefaultPrefix().equals(defaultPrefix))
+                    .filter(bd -> bd.getDefaultPrefix().equals(defaultPrefix))
                     .isPresent();
         }
     }
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java
index 7e312fbc..ca179fc3 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java
+++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java
@@ -44,11 +44,11 @@ class HsBookingItemEntityPatcherUnitTest extends PatchUnitTestBase<
     private static final  Map<String, Object> PATCH_RESOURCES = patchMap(
             entry("CPU", 2),
             entry("HDD", null),
-            entry("SDD", 256)
+            entry("SSD", 256)
     );
     private static final  Map<String, Object> PATCHED_RESOURCES = patchMap(
             entry("CPU", 2),
-            entry("SDD", 256),
+            entry("SSD", 256),
             entry("MEM", 64)
     );
 
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java
index 0d1e22ac..028971ee 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java
+++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java
@@ -6,7 +6,7 @@ import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRepository;
 import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorRepository;
 import net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantRepository;
 import net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleRepository;
-import net.hostsharing.hsadminng.rbac.test.Array;
+import net.hostsharing.hsadminng.mapper.Array;
 import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup;
 import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
 import org.junit.jupiter.api.Nested;
@@ -30,7 +30,7 @@ import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGE
 import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGED_WEBSPACE;
 import static net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantEntity.distinctGrantDisplaysOf;
 import static net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleEntity.distinctRoleNamesOf;
-import static net.hostsharing.hsadminng.rbac.test.Array.fromFormatted;
+import static net.hostsharing.hsadminng.mapper.Array.fromFormatted;
 import static net.hostsharing.hsadminng.rbac.test.JpaAttempt.attempt;
 import static org.assertj.core.api.Assertions.assertThat;
 
@@ -174,9 +174,9 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup
             // then
             allTheseBookingItemsAreReturned(
                     result,
-                    "HsBookingItemEntity(D-1000212:D-1000212 default project, MANAGED_WEBSPACE, [2022-10-01,), some ManagedWebspace, { Daemons: 2, Multi: 4, SDD: 512, Traffic: 12 })",
-                    "HsBookingItemEntity(D-1000212:D-1000212 default project, MANAGED_SERVER, [2022-10-01,), separate ManagedServer, { CPUs: 2, RAM: 8, SDD: 512, Traffic: 42 })",
-                    "HsBookingItemEntity(D-1000212:D-1000212 default project, PRIVATE_CLOUD, [2024-04-01,), some PrivateCloud, { CPUs: 10, HDD: 10240, SDD: 10240, Traffic: 42 })");
+                    "HsBookingItemEntity(D-1000212:D-1000212 default project, MANAGED_WEBSPACE, [2022-10-01,), separate ManagedWebspace, { Daemons: 0, Multi: 1, SSD: 100, Traffic: 50 })",
+                    "HsBookingItemEntity(D-1000212:D-1000212 default project, MANAGED_SERVER, [2022-10-01,), separate ManagedServer, { CPUs: 2, RAM: 8, SSD: 500, Traffic: 500 })",
+                    "HsBookingItemEntity(D-1000212:D-1000212 default project, PRIVATE_CLOUD, [2024-04-01,), some PrivateCloud, { CPUs: 10, HDD: 10000, RAM: 32, SSD: 4000, Traffic: 2000 })");
         }
 
         @Test
@@ -194,9 +194,9 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup
             // then:
             exactlyTheseBookingItemsAreReturned(
                     result,
-                    "HsBookingItemEntity(D-1000111:D-1000111 default project, MANAGED_SERVER, [2022-10-01,), separate ManagedServer, { CPUs: 2, RAM: 8, SDD: 512, Traffic: 42 })",
-                    "HsBookingItemEntity(D-1000111:D-1000111 default project, MANAGED_WEBSPACE, [2022-10-01,), some ManagedWebspace, { Daemons: 2, Multi: 4, SDD: 512, Traffic: 12 })",
-                    "HsBookingItemEntity(D-1000111:D-1000111 default project, PRIVATE_CLOUD, [2024-04-01,), some PrivateCloud, { CPUs: 10, HDD: 10240, SDD: 10240, Traffic: 42 })");
+                    "HsBookingItemEntity(D-1000111:D-1000111 default project, MANAGED_SERVER, [2022-10-01,), separate ManagedServer, { CPUs: 2, RAM: 8, SSD: 500, Traffic: 500 })",
+                    "HsBookingItemEntity(D-1000111:D-1000111 default project, MANAGED_WEBSPACE, [2022-10-01,), separate ManagedWebspace, { Daemons: 0, Multi: 1, SSD: 100, Traffic: 50 })",
+                    "HsBookingItemEntity(D-1000111:D-1000111 default project, PRIVATE_CLOUD, [2024-04-01,), some PrivateCloud, { CPUs: 10, HDD: 10000, RAM: 32, SSD: 4000, Traffic: 2000 })");
         }
     }
 
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidatorUnitTest.java
new file mode 100644
index 00000000..e784edec
--- /dev/null
+++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidatorUnitTest.java
@@ -0,0 +1,55 @@
+package net.hostsharing.hsadminng.hs.booking.item.validators;
+
+import net.hostsharing.hsadminng.hs.booking.debitor.HsBookingDebitorEntity;
+import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
+import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectEntity;
+import org.junit.jupiter.api.Test;
+
+import jakarta.validation.ValidationException;
+
+import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.PRIVATE_CLOUD;
+import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.CLOUD_SERVER;
+import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGED_SERVER;
+import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGED_WEBSPACE;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.catchThrowable;
+
+class HsBookingItemEntityValidatorUnitTest {
+    final HsBookingDebitorEntity debitor = HsBookingDebitorEntity.builder()
+            .debitorNumber(12345)
+            .build();
+    final HsBookingProjectEntity project = HsBookingProjectEntity.builder()
+            .debitor(debitor)
+            .caption("test project")
+            .build();
+
+    @Test
+    void validThrowsException() {
+        // given
+        final var cloudServerBookingItemEntity = HsBookingItemEntity.builder()
+                .type(CLOUD_SERVER)
+                .project(project)
+                .caption("Test-Server")
+                .build();
+
+        // when
+        final var result = catchThrowable( ()-> HsBookingItemEntityValidatorRegistry.validated(cloudServerBookingItemEntity));
+
+        // then
+        assertThat(result).isInstanceOf(ValidationException.class)
+                .hasMessageContaining(
+                        "'D-12345:test project:Test-Server.resources.CPUs' is required but missing",
+                        "'D-12345:test project:Test-Server.resources.RAM' is required but missing",
+                        "'D-12345:test project:Test-Server.resources.SSD' is required but missing",
+                        "'D-12345:test project:Test-Server.resources.Traffic' is required but missing");
+    }
+
+    @Test
+    void listsTypes() {
+        // when
+        final var result = HsBookingItemEntityValidatorRegistry.types();
+
+        // then
+        assertThat(result).containsExactlyInAnyOrder(PRIVATE_CLOUD, CLOUD_SERVER, MANAGED_SERVER, MANAGED_WEBSPACE);
+    }
+}
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidatorsUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidatorsUnitTest.java
deleted file mode 100644
index 741d7c1e..00000000
--- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidatorsUnitTest.java
+++ /dev/null
@@ -1,44 +0,0 @@
-package net.hostsharing.hsadminng.hs.booking.item.validators;
-
-import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
-import org.junit.jupiter.api.Test;
-
-import jakarta.validation.ValidationException;
-
-import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGED_SERVER;
-import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGED_WEBSPACE;
-import static net.hostsharing.hsadminng.hs.booking.item.validators.HsBookingItemEntityValidators.valid;
-import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.CLOUD_SERVER;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.catchThrowable;
-
-class HsBookingItemEntityValidatorsUnitTest {
-
-    @Test
-    void validThrowsException() {
-        // given
-        final var cloudServerBookingItemEntity = HsBookingItemEntity.builder()
-                .type(CLOUD_SERVER)
-                .build();
-
-        // when
-        final var result = catchThrowable( ()-> valid(cloudServerBookingItemEntity) );
-
-        // then
-        assertThat(result).isInstanceOf(ValidationException.class)
-                .hasMessageContaining(
-                        "'resources.CPUs' is required but missing",
-                        "'resources.RAM' is required but missing",
-                        "'resources.SSD' is required but missing",
-                        "'resources.Traffic' is required but missing");
-    }
-
-    @Test
-    void listsTypes() {
-        // when
-        final var result = HsBookingItemEntityValidators.types();
-
-        // then
-        assertThat(result).containsExactlyInAnyOrder(CLOUD_SERVER, MANAGED_SERVER, MANAGED_WEBSPACE);
-    }
-}
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsCloudServerBookingItemValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsCloudServerBookingItemValidatorUnitTest.java
index e15b95d7..787b4c08 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsCloudServerBookingItemValidatorUnitTest.java
+++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsCloudServerBookingItemValidatorUnitTest.java
@@ -1,23 +1,37 @@
 package net.hostsharing.hsadminng.hs.booking.item.validators;
 
+import net.hostsharing.hsadminng.hs.booking.debitor.HsBookingDebitorEntity;
 import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
+import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectEntity;
 import org.junit.jupiter.api.Test;
 
 import java.util.Map;
 
+import static java.util.List.of;
 import static java.util.Map.entry;
+import static java.util.Map.ofEntries;
 import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.CLOUD_SERVER;
-import static net.hostsharing.hsadminng.hs.booking.item.validators.HsBookingItemEntityValidators.forType;
+import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGED_SERVER;
+import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.PRIVATE_CLOUD;
 import static org.assertj.core.api.Assertions.assertThat;
 
 class HsCloudServerBookingItemValidatorUnitTest {
 
+    final HsBookingDebitorEntity debitor = HsBookingDebitorEntity.builder()
+            .debitorNumber(12345)
+            .build();
+    final HsBookingProjectEntity project = HsBookingProjectEntity.builder()
+            .debitor(debitor)
+            .caption("Test-Project")
+            .build();
+
     @Test
     void validatesProperties() {
         // given
-        final var validator = HsBookingItemEntityValidators.forType(CLOUD_SERVER);
         final var cloudServerBookingItemEntity = HsBookingItemEntity.builder()
                 .type(CLOUD_SERVER)
+                .project(project)
+                .caption("Test-Server")
                 .resources(Map.ofEntries(
                         entry("CPUs", 2),
                         entry("RAM", 25),
@@ -28,24 +42,77 @@ class HsCloudServerBookingItemValidatorUnitTest {
                 .build();
 
         // when
-        final var result = validator.validate(cloudServerBookingItemEntity);
+        final var result = HsBookingItemEntityValidatorRegistry.doValidate(cloudServerBookingItemEntity);
 
         // then
-        assertThat(result).containsExactly("'resources.SLA-EMail' is not expected but is set to 'true'");
+        assertThat(result).containsExactly("'D-12345:Test-Project:Test-Server.resources.SLA-EMail' is not expected but is set to 'true'");
     }
 
     @Test
     void containsAllValidations() {
         // when
-        final var validator = forType(CLOUD_SERVER);
+        final var validator = HsBookingItemEntityValidatorRegistry.forType(CLOUD_SERVER);
 
         // then
         assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder(
-                "{type=integer, propertyName=CPUs, required=true, unit=null, min=1, max=32, step=null}",
-                "{type=integer, propertyName=RAM, required=true, unit=GB, min=1, max=128, step=null}",
-                "{type=integer, propertyName=SSD, required=true, unit=GB, min=25, max=1000, step=25}",
-                "{type=integer, propertyName=HDD, required=false, unit=GB, min=0, max=4000, step=250}",
-                "{type=integer, propertyName=Traffic, required=true, unit=GB, min=250, max=10000, step=250}",
-                "{type=enumeration, propertyName=SLA-Infrastructure, required=false, values=[BASIC, EXT8H, EXT4H, EXT2H]}");
+                "{type=integer, propertyName=CPUs, min=1, max=32, required=true, isTotalsValidator=false}",
+                "{type=integer, propertyName=RAM, unit=GB, min=1, max=128, required=true, isTotalsValidator=false}",
+                "{type=integer, propertyName=SSD, unit=GB, min=25, max=1000, step=25, required=true, isTotalsValidator=false}",
+                "{type=integer, propertyName=HDD, unit=GB, min=0, max=4000, step=250, required=false, defaultValue=0, isTotalsValidator=false}",
+                "{type=integer, propertyName=Traffic, unit=GB, min=250, max=10000, step=250, required=true, isTotalsValidator=false}",
+                "{type=enumeration, propertyName=SLA-Infrastructure, values=[BASIC, EXT8H, EXT4H, EXT2H], required=false, isTotalsValidator=false}");
+    }
+
+    @Test
+    void validatesExceedingPropertyTotals() {
+        // given
+        final var subCloudServerBookingItemEntity = HsBookingItemEntity.builder()
+                .type(CLOUD_SERVER)
+                .caption("Test Cloud-Server")
+                .resources(ofEntries(
+                        entry("CPUs", 2),
+                        entry("RAM", 10),
+                        entry("SSD", 50),
+                        entry("Traffic", 2500)
+                ))
+                .build();
+        final HsBookingItemEntity subManagedServerBookingItemEntity = HsBookingItemEntity.builder()
+                .type(MANAGED_SERVER)
+                .caption("Test Managed-Server")
+                .resources(ofEntries(
+                        entry("CPUs", 3),
+                        entry("RAM", 20),
+                        entry("SSD", 100),
+                        entry("Traffic", 3000)
+                ))
+                .build();
+        final var privateCloudBookingItemEntity = HsBookingItemEntity.builder()
+                .type(PRIVATE_CLOUD)
+                .project(project)
+                .caption("Test Cloud")
+                .resources(ofEntries(
+                        entry("CPUs", 4),
+                        entry("RAM", 20),
+                        entry("SSD", 100),
+                        entry("Traffic", 5000)
+                ))
+                .subBookingItems(of(
+                        subManagedServerBookingItemEntity,
+                        subCloudServerBookingItemEntity
+                ))
+                .build();
+        subManagedServerBookingItemEntity.setParentItem(privateCloudBookingItemEntity);
+        subCloudServerBookingItemEntity.setParentItem(privateCloudBookingItemEntity);
+
+        // when
+        final var result = HsBookingItemEntityValidatorRegistry.doValidate(subCloudServerBookingItemEntity);
+
+        // then
+        assertThat(result).containsExactlyInAnyOrder(
+                "'D-12345:Test-Project:Test Cloud.resources.CPUs' maximum total is 4, but actual total CPUs 5",
+                "'D-12345:Test-Project:Test Cloud.resources.RAM' maximum total is 20 GB, but actual total RAM 30 GB",
+                "'D-12345:Test-Project:Test Cloud.resources.SSD' maximum total is 100 GB, but actual total SSD 150 GB",
+                "'D-12345:Test-Project:Test Cloud.resources.Traffic' maximum total is 5000 GB, but actual total Traffic 5500 GB"
+        );
     }
 }
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedServerBookingItemValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedServerBookingItemValidatorUnitTest.java
index 5f2bdfc3..1fe54a82 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedServerBookingItemValidatorUnitTest.java
+++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedServerBookingItemValidatorUnitTest.java
@@ -1,56 +1,228 @@
 package net.hostsharing.hsadminng.hs.booking.item.validators;
 
+import net.hostsharing.hsadminng.hs.booking.debitor.HsBookingDebitorEntity;
 import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
+import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectEntity;
+import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
+import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType;
 import org.junit.jupiter.api.Test;
 
+import java.util.Collection;
+import java.util.List;
 import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
 
+import static java.util.Arrays.stream;
+import static java.util.List.of;
 import static java.util.Map.entry;
+import static java.util.Map.ofEntries;
+import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.CLOUD_SERVER;
 import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGED_SERVER;
-import static net.hostsharing.hsadminng.hs.booking.item.validators.HsBookingItemEntityValidators.forType;
+import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGED_WEBSPACE;
+import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.PRIVATE_CLOUD;
 import static org.assertj.core.api.Assertions.assertThat;
 
 class HsManagedServerBookingItemValidatorUnitTest {
 
+    final HsBookingDebitorEntity debitor = HsBookingDebitorEntity.builder()
+            .debitorNumber(12345)
+            .build();
+    final HsBookingProjectEntity project = HsBookingProjectEntity.builder()
+            .debitor(debitor)
+            .caption("Test-Project")
+            .build();
+
     @Test
     void validatesProperties() {
         // given
-        final var validator = HsBookingItemEntityValidators.forType(MANAGED_SERVER);
         final var mangedServerBookingItemEntity = HsBookingItemEntity.builder()
                 .type(MANAGED_SERVER)
+                .project(project)
                 .resources(Map.ofEntries(
                         entry("CPUs", 2),
                         entry("RAM", 25),
                         entry("SSD", 25),
                         entry("Traffic", 250),
+                        entry("SLA-Platform", "BASIC"),
                         entry("SLA-EMail", true)
                 ))
                 .build();
 
         // when
-        final var result = validator.validate(mangedServerBookingItemEntity);
+        final var result = HsBookingItemEntityValidatorRegistry.doValidate(mangedServerBookingItemEntity);
 
         // then
-        assertThat(result).containsExactly("'resources.SLA-EMail' is expected to be false because resources.SLA-Platform=BASIC but is true");
+        assertThat(result).containsExactly("'D-12345:Test-Project:null.resources.SLA-EMail' is expected to be false because SLA-Platform=BASIC but is true");
     }
 
     @Test
     void containsAllValidations() {
         // when
-        final var validator = forType(MANAGED_SERVER);
+        final var validator = HsBookingItemEntityValidatorRegistry.forType(MANAGED_SERVER);
 
         // then
         assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder(
-                "{type=integer, propertyName=CPUs, required=true, unit=null, min=1, max=32, step=null}",
-                "{type=integer, propertyName=RAM, required=true, unit=GB, min=1, max=128, step=null}",
-                "{type=integer, propertyName=SSD, required=true, unit=GB, min=25, max=1000, step=25}",
-                "{type=integer, propertyName=HDD, required=false, unit=GB, min=0, max=4000, step=250}",
-                "{type=integer, propertyName=Traffic, required=true, unit=GB, min=250, max=10000, step=250}",
-                "{type=enumeration, propertyName=SLA-Platform, required=false, values=[BASIC, EXT8H, EXT4H, EXT2H]}",
-                "{type=boolean, propertyName=SLA-EMail, required=false, falseIf={SLA-Platform=BASIC}}",
-                "{type=boolean, propertyName=SLA-Maria, required=false, falseIf={SLA-Platform=BASIC}}",
-                "{type=boolean, propertyName=SLA-PgSQL, required=false, falseIf={SLA-Platform=BASIC}}",
-                "{type=boolean, propertyName=SLA-Office, required=false, falseIf={SLA-Platform=BASIC}}",
-                "{type=boolean, propertyName=SLA-Web, required=false, falseIf={SLA-Platform=BASIC}}");
+                "{type=integer, propertyName=CPUs, min=1, max=32, required=true, isTotalsValidator=false}",
+                "{type=integer, propertyName=RAM, unit=GB, min=1, max=128, required=true, isTotalsValidator=false}",
+                "{type=integer, propertyName=SSD, unit=GB, min=25, max=1000, step=25, required=true, isTotalsValidator=true, thresholdPercentage=200}",
+                "{type=integer, propertyName=HDD, unit=GB, min=0, max=4000, step=250, required=false, defaultValue=0, isTotalsValidator=true, thresholdPercentage=200}",
+                "{type=integer, propertyName=Traffic, unit=GB, min=250, max=10000, step=250, required=true, isTotalsValidator=true, thresholdPercentage=200}",
+                "{type=enumeration, propertyName=SLA-Platform, values=[BASIC, EXT8H, EXT4H, EXT2H], required=false, defaultValue=BASIC, isTotalsValidator=false}",
+                "{type=boolean, propertyName=SLA-EMail, required=false, defaultValue=false, isTotalsValidator=false}",
+                "{type=boolean, propertyName=SLA-Maria, required=false, isTotalsValidator=false}",
+                "{type=boolean, propertyName=SLA-PgSQL, required=false, isTotalsValidator=false}",
+                "{type=boolean, propertyName=SLA-Office, required=false, isTotalsValidator=false}",
+                "{type=boolean, propertyName=SLA-Web, required=false, isTotalsValidator=false}");
     }
+
+    @Test
+    void validatesExceedingPropertyTotals() {
+        // given
+        final var subCloudServerBookingItemEntity = HsBookingItemEntity.builder()
+                .type(CLOUD_SERVER)
+                .resources(ofEntries(
+                        entry("CPUs", 2),
+                        entry("RAM", 10),
+                        entry("SSD", 50),
+                        entry("Traffic", 2500)
+                ))
+                .build();
+        final HsBookingItemEntity subManagedServerBookingItemEntity = HsBookingItemEntity.builder()
+                .type(MANAGED_SERVER)
+                .resources(ofEntries(
+                        entry("CPUs", 3),
+                        entry("RAM", 20),
+                        entry("SSD", 100),
+                        entry("Traffic", 3000)
+                ))
+                .build();
+        final var privateCloudBookingItemEntity = HsBookingItemEntity.builder()
+                .type(PRIVATE_CLOUD)
+                .project(project)
+                .resources(ofEntries(
+                        entry("CPUs", 4),
+                        entry("RAM", 20),
+                        entry("SSD", 100),
+                        entry("Traffic", 5000)
+                ))
+                .subBookingItems(of(
+                        subManagedServerBookingItemEntity,
+                        subCloudServerBookingItemEntity
+                ))
+                .build();
+
+        subManagedServerBookingItemEntity.setParentItem(privateCloudBookingItemEntity);
+        subCloudServerBookingItemEntity.setParentItem(privateCloudBookingItemEntity);
+
+        // when
+        final var result = HsBookingItemEntityValidatorRegistry.doValidate(subManagedServerBookingItemEntity);
+
+        // then
+        assertThat(result).containsExactlyInAnyOrder(
+                "'D-12345:Test-Project:null.resources.CPUs' maximum total is 4, but actual total CPUs 5",
+                "'D-12345:Test-Project:null.resources.RAM' maximum total is 20 GB, but actual total RAM 30 GB",
+                "'D-12345:Test-Project:null.resources.SSD' maximum total is 100 GB, but actual total SSD 150 GB",
+                "'D-12345:Test-Project:null.resources.Traffic' maximum total is 5000 GB, but actual total Traffic 5500 GB"
+        );
+    }
+
+    @Test
+    void validatesExceedingTotals() {
+        // given
+        final var managedWebspaceBookingItem = HsBookingItemEntity.builder()
+                .type(MANAGED_WEBSPACE)
+                .project(project)
+                .caption("test Managed-Webspace")
+                .resources(ofEntries(
+                        entry("SSD", 100),
+                        entry("Traffic", 1000),
+                        entry("Multi", 1)
+                ))
+                .subHostingAssets(of(
+                        HsHostingAssetEntity.builder()
+                                .type(HsHostingAssetType.MANAGED_WEBSPACE)
+                                .identifier("abc00")
+                                .subHostingAssets(concat(
+                                        generate(26, HsHostingAssetType.UNIX_USER, "xyz00-%c%c"),
+                                        generateDbUsersWithDatabases(3, HsHostingAssetType.PGSQL_USER,
+                                                "xyz00_%c%c",
+                                                1, HsHostingAssetType.PGSQL_DATABASE
+                                        ),
+                                        generateDbUsersWithDatabases(3, HsHostingAssetType.MARIADB_USER,
+                                                "xyz00_%c%c",
+                                                2, HsHostingAssetType.MARIADB_DATABASE
+                                        ),
+                                        generateDomainEmailSetupsWithEMailAddresses(26, HsHostingAssetType.DOMAIN_EMAIL_SETUP,
+                                                "%c%c.example.com",
+                                                10, HsHostingAssetType.EMAIL_ADDRESS
+                                        )
+                                    ))
+                                .build()
+                ))
+                .build();
+
+        // when
+        final var result = HsBookingItemEntityValidatorRegistry.doValidate(managedWebspaceBookingItem);
+
+        // then
+        assertThat(result).containsExactlyInAnyOrder(
+                "'D-12345:Test-Project:test Managed-Webspace.resources.Multi=1 allows at maximum 25 unix users, but 26 found",
+                "'D-12345:Test-Project:test Managed-Webspace.resources.Multi=1 allows at maximum 5 database users, but 6 found",
+                "'D-12345:Test-Project:test Managed-Webspace.resources.Multi=1 allows at maximum 5 databases, but 9 found",
+                "'D-12345:Test-Project:test Managed-Webspace.resources.Multi=1 allows at maximum 250 databases, but 260 found"
+        );
+    }
+
+    @SafeVarargs
+    private List<HsHostingAssetEntity> concat(final List<HsHostingAssetEntity>... hostingAssets) {
+        return stream(hostingAssets)
+                .flatMap(Collection::stream)
+                .collect(Collectors.toList());
+    }
+
+    private List<HsHostingAssetEntity> generate(final int count, final HsHostingAssetType hostingAssetType,
+            final String identifierPattern) {
+        return IntStream.range(0, count)
+                        .mapToObj(number -> HsHostingAssetEntity.builder()
+                                    .type(hostingAssetType)
+                                    .identifier(identifierPattern.formatted((number/'a')+'a', (number%'a')+'a'))
+                                    .build())
+                .toList();
+    }
+
+    private List<HsHostingAssetEntity> generateDbUsersWithDatabases(
+            final int userCount,
+            final HsHostingAssetType directAssetType,
+            final String directAssetIdentifierFormat,
+            final int dbCount,
+            final HsHostingAssetType subAssetType) {
+        return IntStream.range(0, userCount)
+                .mapToObj(n -> HsHostingAssetEntity.builder()
+                        .type(directAssetType)
+                        .identifier(directAssetIdentifierFormat.formatted((n/'a')+'a', (n%'a')+'a'))
+                        .subHostingAssets(
+                                generate(dbCount, subAssetType, "%c%c.example.com".formatted((n/'a')+'a', (n%'a')+'a'))
+                        )
+                        .build())
+                .toList();
+    }
+
+    private List<HsHostingAssetEntity> generateDomainEmailSetupsWithEMailAddresses(
+            final int domainCount,
+            final HsHostingAssetType directAssetType,
+            final String directAssetIdentifierFormat,
+            final int emailAddressCount,
+            final HsHostingAssetType subAssetType) {
+        return IntStream.range(0, domainCount)
+                .mapToObj(n -> HsHostingAssetEntity.builder()
+                        .type(directAssetType)
+                        .identifier(directAssetIdentifierFormat.formatted((n/'a')+'a', (n%'a')+'a'))
+                        .subHostingAssets(
+                                generate(emailAddressCount, subAssetType, "xyz00_%c%c%%c%%c".formatted((n/'a')+'a', (n%'a')+'a'))
+                        )
+                        .build())
+                .toList();
+    }
+
 }
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedWebspaceBookingItemValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedWebspaceBookingItemValidatorUnitTest.java
index 8a278850..dd9081ee 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedWebspaceBookingItemValidatorUnitTest.java
+++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedWebspaceBookingItemValidatorUnitTest.java
@@ -1,54 +1,66 @@
 package net.hostsharing.hsadminng.hs.booking.item.validators;
 
+import net.hostsharing.hsadminng.hs.booking.debitor.HsBookingDebitorEntity;
 import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
+import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectEntity;
 import org.junit.jupiter.api.Test;
 
 import java.util.Map;
 
 import static java.util.Map.entry;
 import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGED_WEBSPACE;
-import static net.hostsharing.hsadminng.hs.booking.item.validators.HsBookingItemEntityValidators.forType;
 import static org.assertj.core.api.Assertions.assertThat;
 
 class HsManagedWebspaceBookingItemValidatorUnitTest {
 
+    final HsBookingDebitorEntity debitor = HsBookingDebitorEntity.builder()
+            .debitorNumber(12345)
+            .build();
+    final HsBookingProjectEntity project = HsBookingProjectEntity.builder()
+            .debitor(debitor)
+            .caption("Test-Project")
+            .build();
+
     @Test
     void validatesProperties() {
         // given
         final var mangedServerBookingItemEntity = HsBookingItemEntity.builder()
                 .type(MANAGED_WEBSPACE)
+                .project(project)
+                .caption("Test Managed-Webspace")
                 .resources(Map.ofEntries(
                         entry("CPUs", 2),
                         entry("RAM", 25),
-                        entry("SSD", 25),
                         entry("Traffic", 250),
                         entry("SLA-EMail", true)
                 ))
                 .build();
-        final var validator = forType(mangedServerBookingItemEntity.getType());
 
         // when
-        final var result = validator.validate(mangedServerBookingItemEntity);
+        final var result = HsBookingItemEntityValidatorRegistry.doValidate(mangedServerBookingItemEntity);
 
         // then
         assertThat(result).containsExactlyInAnyOrder(
-                "'resources.CPUs' is not expected but is set to '2'",
-                "'resources.SLA-EMail' is not expected but is set to 'true'",
-                "'resources.RAM' is not expected but is set to '25'");
+                "'D-12345:Test-Project:Test Managed-Webspace.resources.CPUs' is not expected but is set to '2'",
+                "'D-12345:Test-Project:Test Managed-Webspace.resources.RAM' is not expected but is set to '25'",
+                "'D-12345:Test-Project:Test Managed-Webspace.resources.SSD' is required but missing",
+                "'D-12345:Test-Project:Test Managed-Webspace.resources.SLA-EMail' is not expected but is set to 'true'"
+                );
     }
 
     @Test
     void containsAllValidations() {
         // when
-        final var validator = forType(MANAGED_WEBSPACE);
+        final var validator = HsBookingItemEntityValidatorRegistry.forType(MANAGED_WEBSPACE);
 
         // then
         assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder(
-            "{type=integer, propertyName=SSD, required=true, unit=GB, min=1, max=100, step=1}",
-            "{type=integer, propertyName=HDD, required=false, unit=GB, min=0, max=250, step=10}",
-            "{type=integer, propertyName=Traffic, required=true, unit=GB, min=10, max=1000, step=10}",
-            "{type=enumeration, propertyName=SLA-Platform, required=false, values=[BASIC, EXT24H]}",
-            "{type=integer, propertyName=Daemons, required=false, unit=null, min=0, max=10, step=null}",
-            "{type=boolean, propertyName=Online Office Server, required=false, falseIf=null}");
+                "{type=integer, propertyName=SSD, unit=GB, min=1, max=100, step=1, required=true, isTotalsValidator=false}",
+                "{type=integer, propertyName=HDD, unit=GB, min=0, max=250, step=10, required=false, isTotalsValidator=false}",
+                "{type=integer, propertyName=Traffic, unit=GB, min=10, max=1000, step=10, required=true, isTotalsValidator=false}",
+                "{type=integer, propertyName=Multi, min=1, max=100, step=1, required=false, defaultValue=1, isTotalsValidator=false}",
+                "{type=integer, propertyName=Daemons, min=0, max=10, required=false, defaultValue=0, isTotalsValidator=false}",
+                "{type=boolean, propertyName=Online Office Server, required=false, isTotalsValidator=false}",
+                "{type=enumeration, propertyName=SLA-Platform, values=[BASIC, EXT24H], required=false, defaultValue=BASIC, isTotalsValidator=false}");
     }
 }
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsPrivateCloudBookingItemValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsPrivateCloudBookingItemValidatorUnitTest.java
new file mode 100644
index 00000000..5079f340
--- /dev/null
+++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsPrivateCloudBookingItemValidatorUnitTest.java
@@ -0,0 +1,112 @@
+package net.hostsharing.hsadminng.hs.booking.item.validators;
+
+import net.hostsharing.hsadminng.hs.booking.debitor.HsBookingDebitorEntity;
+import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
+import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectEntity;
+import org.junit.jupiter.api.Test;
+
+import static java.util.List.of;
+import static java.util.Map.entry;
+import static java.util.Map.ofEntries;
+import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.CLOUD_SERVER;
+import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGED_SERVER;
+import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.PRIVATE_CLOUD;
+import static org.assertj.core.api.Assertions.assertThat;
+
+class HsPrivateCloudBookingItemValidatorUnitTest {
+
+    final HsBookingDebitorEntity debitor = HsBookingDebitorEntity.builder()
+            .debitorNumber(12345)
+            .build();
+    final HsBookingProjectEntity project = HsBookingProjectEntity.builder()
+            .debitor(debitor)
+            .caption("Test-Project")
+            .build();
+
+    @Test
+    void validatesPropertyTotals() {
+        // given
+        final var privateCloudBookingItemEntity = HsBookingItemEntity.builder()
+                .type(PRIVATE_CLOUD)
+                .resources(ofEntries(
+                        entry("CPUs", 4),
+                        entry("RAM", 20),
+                        entry("SSD", 100),
+                        entry("Traffic", 5000)
+                ))
+                .subBookingItems(of(
+                        HsBookingItemEntity.builder()
+                                .type(MANAGED_SERVER)
+                                .resources(ofEntries(
+                                        entry("CPUs", 2),
+                                        entry("RAM", 10),
+                                        entry("SSD", 50),
+                                        entry("Traffic", 2500)
+                                ))
+                                .build(),
+                        HsBookingItemEntity.builder()
+                                .type(CLOUD_SERVER)
+                                .resources(ofEntries(
+                                        entry("CPUs", 2),
+                                        entry("RAM", 10),
+                                        entry("SSD", 50),
+                                        entry("Traffic", 2500)
+                                ))
+                                .build()
+                ))
+                .build();
+
+        // when
+        final var result = HsBookingItemEntityValidatorRegistry.doValidate(privateCloudBookingItemEntity);
+
+        // then
+        assertThat(result).isEmpty();
+    }
+
+    @Test
+    void validatesExceedingPropertyTotals() {
+        // given
+        final var privateCloudBookingItemEntity = HsBookingItemEntity.builder()
+                .project(project)
+                .type(PRIVATE_CLOUD)
+                .resources(ofEntries(
+                        entry("CPUs", 4),
+                        entry("RAM", 20),
+                        entry("SSD", 100),
+                        entry("Traffic", 5000)
+                ))
+                .subBookingItems(of(
+                        HsBookingItemEntity.builder()
+                                .type(MANAGED_SERVER)
+                                .resources(ofEntries(
+                                        entry("CPUs", 3),
+                                        entry("RAM", 20),
+                                        entry("SSD", 100),
+                                        entry("Traffic", 3000)
+                                ))
+                                .build(),
+                        HsBookingItemEntity.builder()
+                                .type(CLOUD_SERVER)
+                                .resources(ofEntries(
+                                        entry("CPUs", 2),
+                                        entry("RAM", 10),
+                                        entry("SSD", 50),
+                                        entry("Traffic", 2500)
+                                ))
+                                .build()
+                ))
+                .build();
+
+        // when
+        final var result = HsBookingItemEntityValidatorRegistry.doValidate(privateCloudBookingItemEntity);
+
+        // then
+        assertThat(result).containsExactlyInAnyOrder(
+                "'D-12345:Test-Project:null.resources.CPUs' maximum total is 4, but actual total CPUs 5",
+                "'D-12345:Test-Project:null.resources.RAM' maximum total is 20 GB, but actual total RAM 30 GB",
+                "'D-12345:Test-Project:null.resources.SSD' maximum total is 100 GB, but actual total SSD 150 GB",
+                "'D-12345:Test-Project:null.resources.Traffic' maximum total is 5000 GB, but actual total Traffic 5500 GB"
+        );
+    }
+
+}
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectRepositoryIntegrationTest.java
index 70676f84..e73bf942 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectRepositoryIntegrationTest.java
+++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectRepositoryIntegrationTest.java
@@ -4,7 +4,7 @@ import net.hostsharing.hsadminng.context.Context;
 import net.hostsharing.hsadminng.hs.booking.debitor.HsBookingDebitorRepository;
 import net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantRepository;
 import net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleRepository;
-import net.hostsharing.hsadminng.rbac.test.Array;
+import net.hostsharing.hsadminng.mapper.Array;
 import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup;
 import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
 import org.junit.jupiter.api.Nested;
@@ -23,7 +23,7 @@ import java.util.List;
 
 import static net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantEntity.distinctGrantDisplaysOf;
 import static net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleEntity.distinctRoleNamesOf;
-import static net.hostsharing.hsadminng.rbac.test.Array.fromFormatted;
+import static net.hostsharing.hsadminng.mapper.Array.fromFormatted;
 import static net.hostsharing.hsadminng.rbac.test.JpaAttempt.attempt;
 import static org.assertj.core.api.Assertions.assertThat;
 
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java
index 5204a1ec..e9f8180d 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java
+++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java
@@ -5,6 +5,7 @@ import io.restassured.http.ContentType;
 import net.hostsharing.hsadminng.HsadminNgApplication;
 import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
 import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRepository;
+import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType;
 import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRepository;
 import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorRepository;
 import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup;
@@ -20,8 +21,9 @@ import java.util.Map;
 import java.util.UUID;
 
 import static java.util.Map.entry;
-import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.CLOUD_SERVER;
 import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_SERVER;
+import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_WEBSPACE;
+import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.UNIX_USER;
 import static net.hostsharing.hsadminng.rbac.test.JsonMatcher.lenientlyEquals;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.hamcrest.Matchers.matchesRegex;
@@ -77,25 +79,19 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
                             "type": "MANAGED_WEBSPACE",
                             "identifier": "sec01",
                              "caption": "some Webspace",
-                            "config": {
-                                "extra": 42
-                            }
+                            "config": {}
                         },
                         {
                             "type": "MANAGED_WEBSPACE",
                             "identifier": "fir01",
                             "caption": "some Webspace",
-                            "config": {
-                                "extra": 42
-                            }
+                            "config": {}
                         },
                         {
                             "type": "MANAGED_WEBSPACE",
                             "identifier": "thi01",
                             "caption": "some Webspace",
-                            "config": {
-                                "extra": 42
-                            }
+                            "config": {}
                         }
                     ]
                     """));
@@ -110,41 +106,47 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
 
             RestAssured // @formatter:off
                     .given()
-                    .header("current-user", "superuser-alex@hostsharing.net")
-                    .port(port)
+                        .header("current-user", "superuser-alex@hostsharing.net")
+                        .port(port)
                     .when()
-                    .get("http://localhost/api/hs/hosting/assets?type=" + MANAGED_SERVER)
+                    .   get("http://localhost/api/hs/hosting/assets?type=" + MANAGED_SERVER)
                     .then().log().all().assertThat()
-                    .statusCode(200)
-                    .contentType("application/json")
-                    .body("", lenientlyEquals("""
-                    [
-                        {
-                            "type": "MANAGED_SERVER",
-                            "identifier": "vm1011",
-                            "caption": "some ManagedServer",
-                            "config": {
-                                "extra": 42
+                        .statusCode(200)
+                        .contentType("application/json")
+                        .body("", lenientlyEquals("""
+                        [
+                            {
+                                "type": "MANAGED_SERVER",
+                                "identifier": "vm1011",
+                                "caption": "some ManagedServer",
+                                "config": {
+                                    "monit_max_cpu_usage": 90,
+                                    "monit_max_ram_usage": 80,
+                                    "monit_max_ssd_usage": 70
+                                }
+                            },
+                            {
+                                "type": "MANAGED_SERVER",
+                                "identifier": "vm1012",
+                                "caption": "some ManagedServer",
+                                "config": {
+                                    "monit_max_cpu_usage": 90,
+                                    "monit_max_ram_usage": 80,
+                                    "monit_max_ssd_usage": 70
+                                }
+                            },
+                            {
+                                "type": "MANAGED_SERVER",
+                                "identifier": "vm1013",
+                                "caption": "some ManagedServer",
+                                "config": {
+                                    "monit_max_cpu_usage": 90,
+                                    "monit_max_ram_usage": 80,
+                                    "monit_max_ssd_usage": 70
+                                }
                             }
-                        },
-                        {
-                            "type": "MANAGED_SERVER",
-                            "identifier": "vm1012",
-                            "caption": "some ManagedServer",
-                            "config": {
-                                "extra": 42
-                            }
-                        },
-                        {
-                            "type": "MANAGED_SERVER",
-                            "identifier": "vm1013",
-                            "caption": "some ManagedServer",
-                            "config": {
-                                "extra": 42
-                            }
-                        }
-                    ]
-                    """));
+                        ]
+                        """));
             // @formatter:on
         }
     }
@@ -156,7 +158,14 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
         void globalAdmin_canAddBookedAsset() {
 
             context.define("superuser-alex@hostsharing.net");
-            final var givenBookingItem = givenBookingItem("D-1000111 default project", "some PrivateCloud");
+            final var givenBookingItem = newBookingItem("D-1000111 default project",
+                    HsBookingItemType.MANAGED_WEBSPACE, "separate ManagedWebspace BI",
+                    Map.ofEntries(
+                            entry("SSD", 50),
+                            entry("Traffic", 50)
+                    )
+            );
+            final var givenParentAsset = givenParentAsset(MANAGED_SERVER, "vm1011");
 
             final var location = RestAssured // @formatter:off
                     .given()
@@ -165,12 +174,13 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
                         .body("""
                             {
                                 "bookingItemUuid": "%s",
-                                "type": "MANAGED_SERVER",
-                                "identifier": "vm1400",
-                                "caption": "some new ManagedServer",
-                                "config": { "monit_max_ssd_usage": 80, "monit_max_cpu_usage": 90, "monit_max_ram_usage": 70 }
+                                "type": "MANAGED_WEBSPACE",
+                                "identifier": "fir10",
+                                "parentAssetUuid": "%s",
+                                "caption": "some separate ManagedWebspace HA",
+                                "config": {}
                             }
-                            """.formatted(givenBookingItem.getUuid()))
+                            """.formatted(givenBookingItem.getUuid(), givenParentAsset.getUuid()))
                         .port(port)
                     .when()
                         .post("http://localhost/api/hs/hosting/assets")
@@ -179,19 +189,20 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
                         .contentType(ContentType.JSON)
                         .body("", lenientlyEquals("""
                             {
-                                "type": "MANAGED_SERVER",
-                                "identifier": "vm1400",
-                                "caption": "some new ManagedServer",
-                                "config": { "monit_max_ssd_usage": 80, "monit_max_cpu_usage": 90, "monit_max_ram_usage": 70 }
+                                "type": "MANAGED_WEBSPACE",
+                                "identifier": "fir10",
+                                "caption": "some separate ManagedWebspace HA",
+                                "config": {}
                             }
                             """))
                         .header("Location", matchesRegex("http://localhost:[1-9][0-9]*/api/hs/hosting/assets/[^/]*"))
                     .extract().header("Location");  // @formatter:on
 
             // finally, the new asset can be accessed under the generated UUID
-            final var newUserUuid = UUID.fromString(
+            final var newWebspace = UUID.fromString(
                     location.substring(location.lastIndexOf('/') + 1));
-            assertThat(newUserUuid).isNotNull();
+            assertThat(newWebspace).isNotNull();
+            toCleanup(HsHostingAssetEntity.class, newWebspace);
         }
 
         @Test
@@ -240,7 +251,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
         }
 
         @Test
-        void additionalValidationsArePerformend_whenAddingAsset() {
+        void propertyValidationsArePerformend_whenAddingAsset() {
 
             context.define("superuser-alex@hostsharing.net");
             final var givenBookingItem = givenBookingItem("D-1000111 default project", "some PrivateCloud");
@@ -267,9 +278,66 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
                     .body("", lenientlyEquals("""
                             {
                                 "statusPhrase": "Bad Request",
-                                "message": "['config.extra' is not expected but is set to '42', 'config.monit_max_ssd_usage' is expected to be >= 10 but is 0, 'config.monit_max_cpu_usage' is expected to be <= 100 but is 101, 'config.monit_max_ram_usage' is required but missing]"
+                                "message": "[
+                                          <<<'MANAGED_SERVER:vm1400.config.extra' is not expected but is set to '42',
+                                          <<<'MANAGED_SERVER:vm1400.config.monit_max_ssd_usage' is expected to be >= 10 but is 0,
+                                          <<<'MANAGED_SERVER:vm1400.config.monit_max_cpu_usage' is expected to be <= 100 but is 101,
+                                          <<<'MANAGED_SERVER:vm1400.config.monit_max_ram_usage' is required but missing
+                                          <<<]"
                             }
-                            """));  // @formatter:on
+                            """.replaceAll(" +<<<", "")));  // @formatter:on
+        }
+
+
+        @Test
+        void totalsLimitValidationsArePerformend_whenAddingAsset() {
+
+            context.define("superuser-alex@hostsharing.net");
+            final var givenHostingAsset = givenHostingAsset(MANAGED_WEBSPACE, "fir01");
+            assertThat(givenHostingAsset.getBookingItem().getResources().get("Multi"))
+                    .as("precondition failed")
+                    .isEqualTo(1);
+
+            jpaAttempt.transacted(() -> {
+                context.define("superuser-alex@hostsharing.net");
+                for (int n = 0; n < 25; ++n ) {
+                    toCleanup(assetRepo.save(
+                            HsHostingAssetEntity.builder()
+                                    .type(UNIX_USER)
+                                    .parentAsset(givenHostingAsset)
+                                    .identifier("fir01-%2d".formatted(n))
+                                    .caption("Test UnixUser fir01-%2d".formatted(n))
+                                    .build()));
+                }
+            }).assertSuccessful();
+
+            RestAssured // @formatter:off
+                    .given()
+                        .header("current-user", "superuser-alex@hostsharing.net")
+                        .contentType(ContentType.JSON)
+                        .body("""
+                                    {
+                                        "parentAssetUuid": "%s",
+                                        "type": "UNIX_USER",
+                                        "identifier": "fir01-extra",
+                                        "caption": "some extra UnixUser",
+                                        "config": { }
+                                    }
+                                    """.formatted(givenHostingAsset.getUuid()))
+                        .port(port)
+                    .when()
+                        .post("http://localhost/api/hs/hosting/assets")
+                    .then().log().all().assertThat()
+                        .statusCode(400)
+                        .contentType(ContentType.JSON)
+                        .body("", lenientlyEquals("""
+                                    {
+                                        "statusPhrase": "Bad Request",
+                                        "message": "[
+                                                 <<<'D-1000111:D-1000111 default project:separate ManagedWebspace.resources.Multi=1 allows at maximum 25 unix users, but 26 found
+                                                 <<<]"
+                                    }
+                                    """.replaceAll(" +<<<", "")));  // @formatter:on
         }
     }
 
@@ -295,9 +363,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
                     .body("", lenientlyEquals("""
                         {
                             "caption": "some ManagedServer",
-                            "config": {
-                                 "extra": 42
-                             }
+                            "config": {}
                         }
                     """)); // @formatter:on
         }
@@ -340,9 +406,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
                         {
                             "identifier": "vm1013",
                             "caption": "some ManagedServer",
-                            "config": {
-                                "extra": 42
-                            }
+                            "config": {}
                         }
                     """)); // @formatter:on
         }
@@ -443,6 +507,29 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
         }
     }
 
+    HsHostingAssetEntity givenHostingAsset(final HsHostingAssetType type, final String identifier) {
+        return assetRepo.findByIdentifier(identifier).stream()
+                .filter(ha -> ha.getType()==type)
+                .findAny().orElseThrow();
+    }
+
+    HsBookingItemEntity newBookingItem(
+            final String projectCaption,
+            final HsBookingItemType type, final String bookingItemCaption, final Map<String, Object> resources) {
+        return jpaAttempt.transacted(() -> {
+            context.define("superuser-alex@hostsharing.net");
+            final var project = projectRepo.findByCaption(projectCaption).stream()
+                    .findAny().orElseThrow();
+            final var bookingItem = HsBookingItemEntity.builder()
+                    .project(project)
+                    .type(type)
+                    .caption(bookingItemCaption)
+                    .resources(resources)
+                    .build();
+            return toCleanup(bookingItemRepo.save(bookingItem));
+        }).assertSuccessful().returnedValue();
+    }
+
     HsBookingItemEntity givenBookingItem(final String projectCaption, final String bookingItemCaption) {
         return bookingItemRepo.findByCaption(bookingItemCaption).stream()
                 .filter(bi -> bi.getRelatedProject().getCaption().contains(projectCaption))
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntityPatcherUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntityPatcherUnitTest.java
index d726c9b4..2530f5fa 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntityPatcherUnitTest.java
+++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntityPatcherUnitTest.java
@@ -40,11 +40,11 @@ class HsHostingAssetEntityPatcherUnitTest extends PatchUnitTestBase<
     private static final  Map<String, Object> PATCH_CONFIG = patchMap(
             entry("CPU", 2),
             entry("HDD", null),
-            entry("SDD", 256)
+            entry("SSD", 256)
     );
     private static final  Map<String, Object> PATCHED_CONFIG = patchMap(
             entry("CPU", 2),
-            entry("SDD", 256),
+            entry("SSD", 256),
             entry("MEM", 64)
     );
 
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java
index 55c2e29e..e8195eeb 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java
+++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java
@@ -33,7 +33,8 @@ class HsHostingAssetPropsControllerAcceptanceTest {
                         [
                             "MANAGED_SERVER",
                             "MANAGED_WEBSPACE",
-                            "CLOUD_SERVER"
+                            "CLOUD_SERVER",
+                            "UNIX_USER"
                         ]
                         """));
         // @formatter:on
@@ -55,56 +56,54 @@ class HsHostingAssetPropsControllerAcceptanceTest {
                             {
                                 "type": "integer",
                                 "propertyName": "monit_min_free_ssd",
-                                "required": false,
-                                "unit": null,
                                 "min": 1,
                                 "max": 1000,
-                                "step": null
+                                "required": false,
+                                "isTotalsValidator": false
                             },
                             {
                                 "type": "integer",
                                 "propertyName": "monit_min_free_hdd",
-                                "required": false,
-                                "unit": null,
                                 "min": 1,
                                 "max": 4000,
-                                "step": null
+                                "required": false,
+                                "isTotalsValidator": false
                             },
                             {
                                 "type": "integer",
                                 "propertyName": "monit_max_ssd_usage",
-                                "required": true,
                                 "unit": "%",
                                 "min": 10,
                                 "max": 100,
-                                "step": null
+                                "required": true,
+                                "isTotalsValidator": false
                             },
                             {
                                 "type": "integer",
                                 "propertyName": "monit_max_hdd_usage",
-                                "required": false,
                                 "unit": "%",
                                 "min": 10,
                                 "max": 100,
-                                "step": null
+                                "required": false,
+                                "isTotalsValidator": false
                             },
                             {
                                 "type": "integer",
                                 "propertyName": "monit_max_cpu_usage",
-                                "required": true,
                                 "unit": "%",
                                 "min": 10,
                                 "max": 100,
-                                "step": null
+                                "required": true,
+                                "isTotalsValidator": false
                             },
                             {
                                 "type": "integer",
                                 "propertyName": "monit_max_ram_usage",
-                                "required": true,
                                 "unit": "%",
                                 "min": 10,
                                 "max": 100,
-                                "step": null
+                                "required": true,
+                                "isTotalsValidator": false
                             }
                         ]
                         """));
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRepositoryIntegrationTest.java
index f781046a..83560cc9 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRepositoryIntegrationTest.java
+++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRepositoryIntegrationTest.java
@@ -3,10 +3,11 @@ package net.hostsharing.hsadminng.hs.hosting.asset;
 import net.hostsharing.hsadminng.context.Context;
 import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
 import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRepository;
+import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType;
 import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRepository;
 import net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantRepository;
 import net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleRepository;
-import net.hostsharing.hsadminng.rbac.test.Array;
+import net.hostsharing.hsadminng.mapper.Array;
 import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup;
 import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
 import org.junit.jupiter.api.Nested;
@@ -30,7 +31,7 @@ import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANA
 import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_WEBSPACE;
 import static net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantEntity.distinctGrantDisplaysOf;
 import static net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleEntity.distinctRoleNamesOf;
-import static net.hostsharing.hsadminng.rbac.test.Array.fromFormatted;
+import static net.hostsharing.hsadminng.mapper.Array.fromFormatted;
 import static net.hostsharing.hsadminng.rbac.test.JpaAttempt.attempt;
 import static org.assertj.core.api.Assertions.assertThat;
 
@@ -70,12 +71,13 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu
             // given
             context("superuser-alex@hostsharing.net");
             final var count = assetRepo.count();
-            final var givenManagedServer = givenManagedServer("D-1000111 default project", MANAGED_SERVER);
+            final var givenManagedServer = givenHostingAsset("D-1000111 default project", MANAGED_SERVER);
+            final var newWebspaceBookingItem = newBookingItem(givenManagedServer.getBookingItem(), HsBookingItemType.MANAGED_WEBSPACE, "fir01");
 
             // when
             final var result = attempt(em, () -> {
                 final var newAsset = HsHostingAssetEntity.builder()
-                        .bookingItem(givenManagedServer.getBookingItem())
+                        .bookingItem(newWebspaceBookingItem)
                         .parentAsset(givenManagedServer)
                         .caption("some new managed webspace")
                         .type(MANAGED_WEBSPACE)
@@ -95,18 +97,19 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu
         public void createsAndGrantsRoles() {
             // given
             context("superuser-alex@hostsharing.net");
+            final var givenManagedServer = givenHostingAsset("D-1000111 default project", MANAGED_SERVER);
+            final var newWebspaceBookingItem = newBookingItem(givenManagedServer.getBookingItem(), HsBookingItemType.MANAGED_WEBSPACE, "fir01");
+            em.flush();
             final var initialRoleNames = distinctRoleNamesOf(rawRoleRepo.findAll());
-            final var initialGrantNames = distinctGrantDisplaysOf(rawGrantRepo.findAll()).stream()
-                    .map(s -> s.replace("hs_office_", ""))
-                    .toList();
-            final var givenBookingItem = givenBookingItem("D-1000111 default project", "some PrivateCloud");
+            final var initialGrantNames = distinctGrantDisplaysOf(rawGrantRepo.findAll());
 
             // when
             final var result = attempt(em, () -> {
                 final var newAsset = HsHostingAssetEntity.builder()
-                        .bookingItem(givenBookingItem)
-                        .type(HsHostingAssetType.MANAGED_SERVER)
-                        .identifier("vm9000")
+                        .bookingItem(newWebspaceBookingItem)
+                        .parentAsset(givenManagedServer)
+                        .type(HsHostingAssetType.MANAGED_WEBSPACE)
+                        .identifier("fir00")
                         .caption("some new managed webspace")
                         .build();
                 return toCleanup(assetRepo.save(newAsset));
@@ -117,29 +120,33 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu
             final var all = rawRoleRepo.findAll();
             assertThat(distinctRoleNamesOf(all)).containsExactlyInAnyOrder(Array.from(
                     initialRoleNames,
-                    "hs_hosting_asset#vm9000:OWNER",
-                    "hs_hosting_asset#vm9000:ADMIN",
-                    "hs_hosting_asset#vm9000:AGENT",
-                    "hs_hosting_asset#vm9000:TENANT"));
+                    "hs_hosting_asset#fir00:ADMIN",
+                    "hs_hosting_asset#fir00:AGENT",
+                    "hs_hosting_asset#fir00:OWNER",
+                    "hs_hosting_asset#fir00:TENANT"));
             assertThat(distinctGrantDisplaysOf(rawGrantRepo.findAll()))
-                    .map(s -> s.replace("hs_office_", ""))
                     .containsExactlyInAnyOrder(fromFormatted(
                             initialGrantNames,
 
                             // owner
-                            "{ grant role:hs_hosting_asset#vm9000:OWNER to role:hs_booking_item#somePrivateCloud:ADMIN by system and assume }",
-                            "{ grant perm:hs_hosting_asset#vm9000:DELETE to role:hs_hosting_asset#vm9000:OWNER by system and assume }",
-                            "{ grant role:hs_hosting_asset#vm9000:ADMIN to role:hs_hosting_asset#vm9000:OWNER by system and assume }",
+                            "{ grant role:hs_hosting_asset#fir00:OWNER to role:hs_booking_item#fir01:ADMIN by system and assume }",
+                            "{ grant role:hs_hosting_asset#fir00:OWNER to role:hs_hosting_asset#vm1011:ADMIN by system and assume }",
+                            "{ grant perm:hs_hosting_asset#fir00:DELETE to role:hs_hosting_asset#fir00:OWNER by system and assume }",
 
                             // admin
-                            "{ grant perm:hs_hosting_asset#vm9000:INSERT>hs_hosting_asset to role:hs_hosting_asset#vm9000:ADMIN by system and assume }",
-                            "{ grant perm:hs_hosting_asset#vm9000:UPDATE to role:hs_hosting_asset#vm9000:ADMIN by system and assume }",
-                            "{ grant role:hs_hosting_asset#vm9000:ADMIN to role:hs_booking_item#somePrivateCloud:AGENT by system and assume }",
-                            "{ grant role:hs_hosting_asset#vm9000:TENANT to role:hs_hosting_asset#vm9000:AGENT by system and assume }",
-                            "{ grant role:hs_hosting_asset#vm9000:AGENT to role:hs_hosting_asset#vm9000:ADMIN by system and assume }",
+                            "{ grant role:hs_hosting_asset#fir00:ADMIN to role:hs_hosting_asset#fir00:OWNER by system and assume }",
+                            "{ grant role:hs_hosting_asset#fir00:ADMIN to role:hs_booking_item#fir01:AGENT by system and assume }",
+                            "{ grant perm:hs_hosting_asset#fir00:INSERT>hs_hosting_asset to role:hs_hosting_asset#fir00:ADMIN by system and assume }",
+                            "{ grant perm:hs_hosting_asset#fir00:UPDATE to role:hs_hosting_asset#fir00:ADMIN by system and assume }",
+
+                            // agent
+                            "{ grant role:hs_hosting_asset#fir00:ADMIN to role:hs_hosting_asset#vm1011:AGENT by system and assume }",
+                            "{ grant role:hs_hosting_asset#fir00:AGENT to role:hs_hosting_asset#fir00:ADMIN by system and assume }",
 
                             // tenant
-                            "{ grant role:hs_booking_item#somePrivateCloud:TENANT to role:hs_hosting_asset#vm9000:TENANT by system and assume }",
+                            "{ grant role:hs_booking_item#fir01:TENANT to role:hs_hosting_asset#fir00:TENANT by system and assume }",
+                            "{ grant role:hs_hosting_asset#fir00:TENANT to role:hs_hosting_asset#fir00:AGENT by system and assume }",
+                            "{ grant role:hs_hosting_asset#vm1011:TENANT to role:hs_hosting_asset#fir00:TENANT by system and assume }",
 
                             null));
         }
@@ -164,9 +171,9 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu
             // then
             allTheseServersAreReturned(
                     result,
-                    "HsHostingAssetEntity(MANAGED_WEBSPACE, sec01, some Webspace, MANAGED_SERVER:vm1012, D-1000212:D-1000212 default project:separate ManagedServer, { extra: 42 })",
-                    "HsHostingAssetEntity(MANAGED_WEBSPACE, thi01, some Webspace, MANAGED_SERVER:vm1013, D-1000313:D-1000313 default project:separate ManagedServer, { extra: 42 })",
-                    "HsHostingAssetEntity(MANAGED_WEBSPACE, fir01, some Webspace, MANAGED_SERVER:vm1011, D-1000111:D-1000111 default project:separate ManagedServer, { extra: 42 })");
+                    "HsHostingAssetEntity(MANAGED_WEBSPACE, sec01, some Webspace, MANAGED_SERVER:vm1012, D-1000212:D-1000212 default project:separate ManagedWebspace)",
+                    "HsHostingAssetEntity(MANAGED_WEBSPACE, fir01, some Webspace, MANAGED_SERVER:vm1011, D-1000111:D-1000111 default project:separate ManagedWebspace)",
+                    "HsHostingAssetEntity(MANAGED_WEBSPACE, thi01, some Webspace, MANAGED_SERVER:vm1013, D-1000313:D-1000313 default project:separate ManagedWebspace)");
         }
 
         @Test
@@ -182,9 +189,8 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu
             // then:
             exactlyTheseAssetsAreReturned(
                     result,
-                    "HsHostingAssetEntity(MANAGED_WEBSPACE, fir01, some Webspace, MANAGED_SERVER:vm1011, D-1000111:D-1000111 default project:separate ManagedServer, { extra: 42 })",
-                    "HsHostingAssetEntity(MANAGED_SERVER, vm1011, some ManagedServer, D-1000111:D-1000111 default project:some PrivateCloud, { extra: 42 })",
-                    "HsHostingAssetEntity(CLOUD_SERVER, vm2011, another CloudServer, D-1000111:D-1000111 default project:some PrivateCloud, { extra: 42 })");
+                    "HsHostingAssetEntity(MANAGED_WEBSPACE, fir01, some Webspace, MANAGED_SERVER:vm1011, D-1000111:D-1000111 default project:separate ManagedWebspace)",
+                    "HsHostingAssetEntity(MANAGED_SERVER, vm1011, some ManagedServer, D-1000111:D-1000111 default project:separate ManagedServer, { monit_max_cpu_usage: 90, monit_max_ram_usage: 80, monit_max_ssd_usage: 70 })");
         }
 
         @Test
@@ -200,7 +206,7 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu
             // then
             allTheseServersAreReturned(
                     result,
-                    "HsHostingAssetEntity(MANAGED_WEBSPACE, thi01, some Webspace, MANAGED_SERVER:vm1013, D-1000313:D-1000313 default project:separate ManagedServer, { extra: 42 })");
+                    "HsHostingAssetEntity(MANAGED_WEBSPACE, sec01, some Webspace, MANAGED_SERVER:vm1012, D-1000212:D-1000212 default project:separate ManagedWebspace)");
         }
 
     }
@@ -351,7 +357,7 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu
     private HsHostingAssetEntity givenSomeTemporaryAsset(final String projectCaption, final String identifier) {
         return jpaAttempt.transacted(() -> {
             context("superuser-alex@hostsharing.net");
-            final var givenBookingItem = givenBookingItem("D-1000111 default project", "some PrivateCloud");
+            final var givenBookingItem = givenBookingItem("D-1000111 default project", "test CloudServer");
             final var newAsset = HsHostingAssetEntity.builder()
                     .bookingItem(givenBookingItem)
                     .type(CLOUD_SERVER)
@@ -367,20 +373,30 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu
     }
 
     HsBookingItemEntity givenBookingItem(final String projectCaption, final String bookingItemCaption) {
-        final var givenProject = projectRepo.findByCaption(projectCaption).stream()
-                .findAny().orElseThrow();
-        return bookingItemRepo.findAllByProjectUuid(givenProject.getUuid()).stream()
-                .filter(i -> i.getCaption().equals(bookingItemCaption))
+        return bookingItemRepo.findByCaption(bookingItemCaption).stream()
+                .filter(i -> i.getRelatedProject().getCaption().equals(projectCaption))
                 .findAny().orElseThrow();
     }
 
-    HsHostingAssetEntity givenManagedServer(final String projectCaption, final HsHostingAssetType type) {
+    HsHostingAssetEntity givenHostingAsset(final String projectCaption, final HsHostingAssetType type) {
         final var givenProject = projectRepo.findByCaption(projectCaption).stream()
                 .findAny().orElseThrow();
         return assetRepo.findAllByCriteria(givenProject.getUuid(), null, type).stream()
                 .findAny().orElseThrow();
     }
 
+    HsBookingItemEntity newBookingItem(
+            final HsBookingItemEntity parentBookingItem,
+            final HsBookingItemType type,
+            final String caption) {
+        final var newBookingItem = HsBookingItemEntity.builder()
+                .parentItem(parentBookingItem)
+                .type(type)
+                .caption(caption)
+                .build();
+        return toCleanup(bookingItemRepo.save(newBookingItem));
+    }
+
     void exactlyTheseAssetsAreReturned(
             final List<HsHostingAssetEntity> actualResult,
             final String... serverNames) {
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsCloudServerHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsCloudServerHostingAssetValidatorUnitTest.java
index de679c40..ee6644e0 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsCloudServerHostingAssetValidatorUnitTest.java
+++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsCloudServerHostingAssetValidatorUnitTest.java
@@ -7,7 +7,6 @@ import java.util.Map;
 
 import static java.util.Map.entry;
 import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.CLOUD_SERVER;
-import static net.hostsharing.hsadminng.hs.hosting.asset.validators.HsHostingAssetEntityValidators.forType;
 import static org.assertj.core.api.Assertions.assertThat;
 
 class HsCloudServerHostingAssetValidatorUnitTest {
@@ -17,24 +16,25 @@ class HsCloudServerHostingAssetValidatorUnitTest {
         // given
         final var cloudServerHostingAssetEntity = HsHostingAssetEntity.builder()
                 .type(CLOUD_SERVER)
+                .identifier("vm1234")
                 .config(Map.ofEntries(
                         entry("RAM", 2000)
                 ))
                 .build();
-        final var validator = forType(cloudServerHostingAssetEntity.getType());
+        final var validator = HsHostingAssetEntityValidatorRegistry.forType(cloudServerHostingAssetEntity.getType());
 
 
         // when
         final var result = validator.validate(cloudServerHostingAssetEntity);
 
         // then
-        assertThat(result).containsExactly("'config.RAM' is not expected but is set to '2000'");
+        assertThat(result).containsExactly("'CLOUD_SERVER:vm1234.config.RAM' is not expected but is set to '2000'");
     }
 
     @Test
     void containsAllValidations() {
         // when
-        final var validator = forType(CLOUD_SERVER);
+        final var validator = HsHostingAssetEntityValidatorRegistry.forType(CLOUD_SERVER);
 
         // then
         assertThat(validator.properties()).map(Map::toString).isEmpty();
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorsUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorUnitTest.java
similarity index 60%
rename from src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorsUnitTest.java
rename to src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorUnitTest.java
index 0e07e30c..b92e5dc9 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorsUnitTest.java
+++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorUnitTest.java
@@ -6,27 +6,27 @@ import org.junit.jupiter.api.Test;
 import jakarta.validation.ValidationException;
 
 import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_SERVER;
-import static net.hostsharing.hsadminng.hs.hosting.asset.validators.HsHostingAssetEntityValidators.valid;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.catchThrowable;
 
-class HsHostingAssetEntityValidatorsUnitTest {
+class HsHostingAssetEntityValidatorUnitTest {
 
     @Test
     void validThrowsException() {
         // given
         final var managedServerHostingAssetEntity = HsHostingAssetEntity.builder()
                 .type(MANAGED_SERVER)
+                .identifier("vm1234")
                 .build();
 
         // when
-        final var result = catchThrowable( ()-> valid(managedServerHostingAssetEntity) );
+        final var result = catchThrowable( ()-> HsHostingAssetEntityValidatorRegistry.validated(managedServerHostingAssetEntity));
 
         // then
         assertThat(result).isInstanceOf(ValidationException.class)
                 .hasMessageContaining(
-                        "'config.monit_max_ssd_usage' is required but missing",
-                        "'config.monit_max_cpu_usage' is required but missing",
-                        "'config.monit_max_ram_usage' is required but missing");
+                        "'MANAGED_SERVER:vm1234.config.monit_max_ssd_usage' is required but missing",
+                        "'MANAGED_SERVER:vm1234.config.monit_max_cpu_usage' is required but missing",
+                        "'MANAGED_SERVER:vm1234.config.monit_max_ram_usage' is required but missing");
     }
 }
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidatorUnitTest.java
index cb9e066b..b8e75436 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidatorUnitTest.java
+++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidatorUnitTest.java
@@ -7,7 +7,6 @@ import java.util.Map;
 
 import static java.util.Map.entry;
 import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_SERVER;
-import static net.hostsharing.hsadminng.hs.hosting.asset.validators.HsHostingAssetEntityValidators.forType;
 import static org.assertj.core.api.Assertions.assertThat;
 
 class HsManagedServerHostingAssetValidatorUnitTest {
@@ -17,22 +16,23 @@ class HsManagedServerHostingAssetValidatorUnitTest {
         // given
         final var mangedWebspaceHostingAssetEntity = HsHostingAssetEntity.builder()
                 .type(MANAGED_SERVER)
+                .identifier("vm1234")
                 .config(Map.ofEntries(
                         entry("monit_max_hdd_usage", "90"),
                         entry("monit_max_cpu_usage", 2),
                         entry("monit_max_ram_usage", 101)
                 ))
                 .build();
-        final var validator = forType(mangedWebspaceHostingAssetEntity.getType());
+        final var validator = HsHostingAssetEntityValidatorRegistry.forType(mangedWebspaceHostingAssetEntity.getType());
 
         // when
         final var result = validator.validate(mangedWebspaceHostingAssetEntity);
 
         // then
         assertThat(result).containsExactlyInAnyOrder(
-                "'config.monit_max_ssd_usage' is required but missing",
-                "'config.monit_max_hdd_usage' is expected to be of type class java.lang.Integer, but is of type 'String'",
-                "'config.monit_max_cpu_usage' is expected to be >= 10 but is 2",
-                "'config.monit_max_ram_usage' is expected to be <= 100 but is 101");
+                "'MANAGED_SERVER:vm1234.config.monit_max_cpu_usage' is expected to be >= 10 but is 2",
+                "'MANAGED_SERVER:vm1234.config.monit_max_ram_usage' is expected to be <= 100 but is 101",
+                "'MANAGED_SERVER:vm1234.config.monit_max_ssd_usage' is required but missing",
+                "'MANAGED_SERVER:vm1234.config.monit_max_hdd_usage' is expected to be of type class java.lang.Integer, but is of type 'String'");
     }
 }
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedWebspaceHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedWebspaceHostingAssetValidatorUnitTest.java
index 83634501..d2e74894 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedWebspaceHostingAssetValidatorUnitTest.java
+++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedWebspaceHostingAssetValidatorUnitTest.java
@@ -1,13 +1,14 @@
 package net.hostsharing.hsadminng.hs.hosting.asset.validators;
 
 import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
+import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType;
 import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
+import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType;
 import org.junit.jupiter.api.Test;
 
 import java.util.Map;
 
 import static java.util.Map.entry;
-import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_SERVER;
 import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_WEBSPACE;
 import static org.assertj.core.api.Assertions.assertThat;
 import static net.hostsharing.hsadminng.hs.booking.project.TestHsBookingProject.TEST_PROJECT;
@@ -16,21 +17,32 @@ class HsManagedWebspaceHostingAssetValidatorUnitTest {
 
     final HsBookingItemEntity managedServerBookingItem = HsBookingItemEntity.builder()
             .project(TEST_PROJECT)
+            .type(HsBookingItemType.MANAGED_SERVER)
+            .caption("Test Managed-Server")
+            .resources(Map.ofEntries(
+                    entry("CPUs", 2),
+                    entry("RAM", 25),
+                    entry("SSD", 25),
+                    entry("Traffic", 250),
+                    entry("SLA-Platform", "EXT4H"),
+                    entry("SLA-EMail", true)
+            ))
             .build();
     final HsHostingAssetEntity mangedServerAssetEntity = HsHostingAssetEntity.builder()
-            .type(MANAGED_SERVER)
+            .type(HsHostingAssetType.MANAGED_SERVER)
             .bookingItem(managedServerBookingItem)
+            .identifier("vm1234")
             .config(Map.ofEntries(
-                    entry("HDD", 0),
-                    entry("SSD", 1),
-                    entry("Traffic", 10)
+                    entry("monit_max_ssd_usage", 70),
+                    entry("monit_max_cpu_usage", 80),
+                    entry("monit_max_ram_usage", 90)
             ))
             .build();
 
     @Test
     void validatesIdentifier() {
         // given
-        final var validator = HsHostingAssetEntityValidators.forType(MANAGED_WEBSPACE);
+        final var validator = HsHostingAssetEntityValidatorRegistry.forType(MANAGED_WEBSPACE);
         final var mangedWebspaceHostingAssetEntity = HsHostingAssetEntity.builder()
                 .type(MANAGED_WEBSPACE)
                 .parentAsset(mangedServerAssetEntity)
@@ -47,7 +59,7 @@ class HsManagedWebspaceHostingAssetValidatorUnitTest {
     @Test
     void validatesUnknownProperties() {
         // given
-        final var validator = HsHostingAssetEntityValidators.forType(MANAGED_WEBSPACE);
+        final var validator = HsHostingAssetEntityValidatorRegistry.forType(MANAGED_WEBSPACE);
         final var mangedWebspaceHostingAssetEntity = HsHostingAssetEntity.builder()
                 .type(MANAGED_WEBSPACE)
                 .parentAsset(mangedServerAssetEntity)
@@ -61,13 +73,13 @@ class HsManagedWebspaceHostingAssetValidatorUnitTest {
         final var result = validator.validate(mangedWebspaceHostingAssetEntity);
 
         // then
-        assertThat(result).containsExactly("'config.unknown' is not expected but is set to 'some value'");
+        assertThat(result).containsExactly("'MANAGED_WEBSPACE:abc00.config.unknown' is not expected but is set to 'some value'");
     }
 
     @Test
     void validatesValidEntity() {
         // given
-        final var validator = HsHostingAssetEntityValidators.forType(MANAGED_WEBSPACE);
+        final var validator = HsHostingAssetEntityValidatorRegistry.forType(MANAGED_WEBSPACE);
         final var mangedWebspaceHostingAssetEntity = HsHostingAssetEntity.builder()
                 .type(MANAGED_WEBSPACE)
                 .parentAsset(mangedServerAssetEntity)
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountRepositoryIntegrationTest.java
index c46210c4..d7d07f69 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountRepositoryIntegrationTest.java
+++ b/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountRepositoryIntegrationTest.java
@@ -4,7 +4,7 @@ import net.hostsharing.hsadminng.context.Context;
 import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup;
 import net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantRepository;
 import net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleRepository;
-import net.hostsharing.hsadminng.rbac.test.Array;
+import net.hostsharing.hsadminng.mapper.Array;
 import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
 import org.apache.commons.lang3.RandomStringUtils;
 import org.junit.jupiter.api.Nested;
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactRepositoryIntegrationTest.java
index cca5c48c..4e591973 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactRepositoryIntegrationTest.java
+++ b/src/test/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactRepositoryIntegrationTest.java
@@ -4,7 +4,7 @@ import net.hostsharing.hsadminng.context.Context;
 import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup;
 import net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantRepository;
 import net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleRepository;
-import net.hostsharing.hsadminng.rbac.test.Array;
+import net.hostsharing.hsadminng.mapper.Array;
 import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
 import org.apache.commons.lang3.RandomStringUtils;
 import org.junit.jupiter.api.Nested;
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionRepositoryIntegrationTest.java
index ff6c9315..0c9215f9 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionRepositoryIntegrationTest.java
+++ b/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionRepositoryIntegrationTest.java
@@ -5,7 +5,7 @@ import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipReposito
 import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup;
 import net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantRepository;
 import net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleRepository;
-import net.hostsharing.hsadminng.rbac.test.Array;
+import net.hostsharing.hsadminng.mapper.Array;
 import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionRepositoryIntegrationTest.java
index 65f85b58..e6163cd4 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionRepositoryIntegrationTest.java
+++ b/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionRepositoryIntegrationTest.java
@@ -5,7 +5,7 @@ import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipReposito
 import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup;
 import net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantRepository;
 import net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleRepository;
-import net.hostsharing.hsadminng.rbac.test.Array;
+import net.hostsharing.hsadminng.mapper.Array;
 import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorRepositoryIntegrationTest.java
index b2e54d06..856356cf 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorRepositoryIntegrationTest.java
+++ b/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorRepositoryIntegrationTest.java
@@ -11,7 +11,7 @@ import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup;
 import net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantRepository;
 import net.hostsharing.hsadminng.rbac.rbacgrant.RbacGrantsDiagramService;
 import net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleRepository;
-import net.hostsharing.hsadminng.rbac.test.Array;
+import net.hostsharing.hsadminng.mapper.Array;
 import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
 import org.hibernate.Hibernate;
 import org.junit.jupiter.api.Disabled;
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipRepositoryIntegrationTest.java
index 1cba78da..701e6651 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipRepositoryIntegrationTest.java
+++ b/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipRepositoryIntegrationTest.java
@@ -7,7 +7,7 @@ import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerRepository;
 import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup;
 import net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantRepository;
 import net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleRepository;
-import net.hostsharing.hsadminng.rbac.test.Array;
+import net.hostsharing.hsadminng.mapper.Array;
 import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
 import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerRepositoryIntegrationTest.java
index 39faf7eb..5daf0f8f 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerRepositoryIntegrationTest.java
+++ b/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerRepositoryIntegrationTest.java
@@ -30,7 +30,7 @@ import java.util.Objects;
 import static net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantEntity.distinctGrantDisplaysOf;
 import static net.hostsharing.hsadminng.rbac.rbacrole.RawRbacObjectEntity.objectDisplaysOf;
 import static net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleEntity.distinctRoleNamesOf;
-import static net.hostsharing.hsadminng.rbac.test.Array.from;
+import static net.hostsharing.hsadminng.mapper.Array.from;
 import static net.hostsharing.hsadminng.rbac.test.JpaAttempt.attempt;
 import static org.assertj.core.api.Assertions.assertThat;
 
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonRepositoryIntegrationTest.java
index 7ce2fdf1..efd7064f 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonRepositoryIntegrationTest.java
+++ b/src/test/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonRepositoryIntegrationTest.java
@@ -4,7 +4,7 @@ import net.hostsharing.hsadminng.context.Context;
 import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup;
 import net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantRepository;
 import net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleRepository;
-import net.hostsharing.hsadminng.rbac.test.Array;
+import net.hostsharing.hsadminng.mapper.Array;
 import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
 import org.apache.commons.lang3.RandomStringUtils;
 import org.junit.jupiter.api.Nested;
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRepositoryIntegrationTest.java
index fe9e2ef1..0792d656 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRepositoryIntegrationTest.java
+++ b/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRepositoryIntegrationTest.java
@@ -6,7 +6,7 @@ import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRepository;
 import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup;
 import net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantRepository;
 import net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleRepository;
-import net.hostsharing.hsadminng.rbac.test.Array;
+import net.hostsharing.hsadminng.mapper.Array;
 import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
 import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateRepositoryIntegrationTest.java
index 5544a3e3..ad7ee76e 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateRepositoryIntegrationTest.java
+++ b/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateRepositoryIntegrationTest.java
@@ -7,7 +7,7 @@ import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorRepository;
 import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup;
 import net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantRepository;
 import net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleRepository;
-import net.hostsharing.hsadminng.rbac.test.Array;
+import net.hostsharing.hsadminng.mapper.Array;
 import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
 import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
@@ -26,7 +26,7 @@ import java.util.List;
 
 import static net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantEntity.distinctGrantDisplaysOf;
 import static net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleEntity.distinctRoleNamesOf;
-import static net.hostsharing.hsadminng.rbac.test.Array.fromFormatted;
+import static net.hostsharing.hsadminng.mapper.Array.fromFormatted;
 import static net.hostsharing.hsadminng.rbac.test.JpaAttempt.attempt;
 import static org.assertj.core.api.Assertions.assertThat;
 
diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/context/ContextIntegrationTests.java b/src/test/java/net/hostsharing/hsadminng/rbac/context/ContextIntegrationTests.java
index 11cda37f..22e1df04 100644
--- a/src/test/java/net/hostsharing/hsadminng/rbac/context/ContextIntegrationTests.java
+++ b/src/test/java/net/hostsharing/hsadminng/rbac/context/ContextIntegrationTests.java
@@ -2,7 +2,7 @@ package net.hostsharing.hsadminng.rbac.context;
 
 import net.hostsharing.hsadminng.context.Context;
 import net.hostsharing.hsadminng.mapper.Mapper;
-import net.hostsharing.hsadminng.rbac.test.Array;
+import net.hostsharing.hsadminng.mapper.Array;
 import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RbacRoleRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RbacRoleRepositoryIntegrationTest.java
index 536d748c..e7a28261 100644
--- a/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RbacRoleRepositoryIntegrationTest.java
+++ b/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RbacRoleRepositoryIntegrationTest.java
@@ -1,7 +1,7 @@
 package net.hostsharing.hsadminng.rbac.rbacrole;
 
 import net.hostsharing.hsadminng.context.Context;
-import net.hostsharing.hsadminng.rbac.test.Array;
+import net.hostsharing.hsadminng.mapper.Array;
 import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
 import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserRepositoryIntegrationTest.java
index f1e6fef5..be6377a0 100644
--- a/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserRepositoryIntegrationTest.java
+++ b/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserRepositoryIntegrationTest.java
@@ -2,7 +2,7 @@ package net.hostsharing.hsadminng.rbac.rbacuser;
 
 import net.hostsharing.hsadminng.context.Context;
 import net.hostsharing.hsadminng.rbac.context.ContextBasedTest;
-import net.hostsharing.hsadminng.rbac.test.Array;
+import net.hostsharing.hsadminng.mapper.Array;
 import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
 import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;