diff --git a/Documentation/Examples/geometry_example.cc b/Documentation/Examples/geometry_example.cc
index 6a05974fd6d5e0224d809072b99979e29578f7d2..4091ab904c9dc7a8b233b21af78f2f45b0874ad9 100644
--- a/Documentation/Examples/geometry_example.cc
+++ b/Documentation/Examples/geometry_example.cc
@@ -39,10 +39,10 @@ int main() {
   std::cout << "p2-p1 norm^2: " << norm << std::endl;
 
   Sphere s(p1, 10_m); // define a sphere around a point with a radius
-  std::cout << "p1 inside s:  " << s.isInside(p2) << std::endl;
+  std::cout << "p1 inside s:  " << s.Contains(p2) << std::endl;
 
   Sphere s2(p1, 3_um); // another sphere
-  std::cout << "p1 inside s2: " << s2.isInside(p2) << std::endl;
+  std::cout << "p1 inside s2: " << s2.Contains(p2) << std::endl;
 
   // let's try parallel projections:
   auto const v1 = Vector<length_d>(root, {1_m, 1_m, 0_m});
diff --git a/Environment/BaseEnvironment.h b/Environment/BaseEnvironment.h
deleted file mode 100644
index e85febecad782321c189145fb57e0fc9534e671f..0000000000000000000000000000000000000000
--- a/Environment/BaseEnvironment.h
+++ /dev/null
@@ -1,57 +0,0 @@
-#ifndef _include_EnvironmentBase_h
-#define _include_EnvironmentBase_h
-
-#include <vector>
-#include <memory>
-#include <algorithm>
-#include <corsika/geometry/Sphere.h>
-#include <corsika/geometry/Point.h>
-
-namespace corsika::environment {
-
-class BaseEnvironment {
-    std::vector<std::unique_ptr<BaseEnvironment>> childNodes, excludedOverlapNodes;
-    corsika::geometry::Sphere shape;
-    
-public:
-    bool ShapeContains(corsika::geometry::Point const& p) const {
-        return shape.Contains(p);
-    }
-
-    BaseEnvironment* GetContainingNode(corsika::geometry::Point const& p) const {
-        if (!shape.Contains(p))
-        {
-            return nullptr;
-        }
-            
-        auto const predicate = [&] (auto const& s) {return bool(s->GetContainingNode(p));};
-        
-        auto const childFountIt = std::find_if(childNodes.cbegin(), childNodes.cend(), predicate);
-        
-        if (childFoundIt == childNodes.cend())
-        {
-            if (auto const excludedIt = std::find_if(excludedOverlapNodes.cbegin(), excludedOverlapNodes.cend(), predicate); excluded == excludedOverlapNodes.cend())
-            {
-                return *this;
-            }
-            else
-            {
-                return excludedIt;
-            }
-        }
-        
-        return (*childFoundIt)->GetContainingNode(p);
-    }
-
-    /**
-     * Get mass density at given point \a p. It is the caller's responsibility
-     * to call this method from the appropriate node in whose domain \a p is located.
-     */
-    virtual MassDensityType GetDensity(corsika::geometry::Point const& p) = 0;
-    
-    virtual std::pair<corsika::particles::Code const*, corsika::particles::Code const*> GetTargetComposition(Point const& p)
-};
-
-}
-
-#endif
diff --git a/Environment/VolumeTreeNode.h b/Environment/VolumeTreeNode.h
new file mode 100644
index 0000000000000000000000000000000000000000..2dcc0caf60c8456229ed29aafd02d633680a7037
--- /dev/null
+++ b/Environment/VolumeTreeNode.h
@@ -0,0 +1,113 @@
+#ifndef _include_VolumeTreeNode_H
+#define _include_VolumeTreeNode_H
+
+namespace corsika::environment {
+
+class Empty {}; //<! intended for usage as default template arguments
+
+template <typename IModelProperties=Empty>
+class VolumeTreeNode {
+public:
+    using VTNUPtr = std::unique_ptr<VolumeTreeNode<IModelProperties>>;
+    using IMPSharedPtr = std::shared_ptr<IModelProperties>;
+    
+    VolumeTreeNode(VolUPtr pVolume = nullptr) : fGeoVolume(std::move(pVolume)) { }
+
+    bool Contains(Point const& p) const { return fGeoVolume->Contains(p); }
+    
+    VolumeTreeNode<IModelProperties> const* Excludes(Point const& p) const {
+        auto exclContainsIter = std::find_if(fExcludedNodes.cbegin(), fExcludedNodes.cend(),
+            [&] (auto const& s) {return bool(s->Contains(p));});
+        
+        return exclContainsIter != fExcludedNodes.cend() ? *exclContainsIter : nullptr;
+    }
+
+    /** returns a pointer to the sub-VolumeTreeNode which is "responsible" for the given
+     * \class Point \arg p, or nullptr iff \arg p is not contained in this volume.
+     */
+    VolumeTreeNode<IModelProperties> const* GetContainingNode(Point const& p) const {
+        if (!Contains(p))
+        {
+            return nullptr;
+        }
+        
+        if (auto const childContainsIter = std::find_if(fChildNodes.cbegin(), fChildNodes.cend(),
+            [&] (auto const& s) {return bool(s->Contains(p));});
+            childContainsIter == fChildNodes.cend()) // not contained in any of the children
+        {
+            if (auto const exclContainsIter = Excludes(p)) // contained in any excluded nodes
+            {
+                return exclContainsIter->GetContainingNode(p);
+            }
+            else
+            {
+                return this;
+            }
+        }
+        else 
+        {
+            return (*childContainsIter)->GetContainingNode(p);
+        }
+    }
+    
+    void addChild(VTNUPtr pChild) {
+        pChild->fParentNode = this;
+        fChildNodes.push_back(std::move(pChild));
+        // It is a bad idea to return an iterator to the inserted element
+        // because it might get invalidated when the vector needs to grow
+        // later and the caller won't notice.
+    }
+    
+    void excludeOverlapWith(VTNUPtr const& pNode) {        
+        fExcludedNodes.push_back(pNode.get());
+    }
+    
+    auto GetParent() const {
+        return fParentNode;
+    };
+    
+    auto const& GetVolume() const {
+        return *fGeoVolume;
+    }
+    
+    auto const& GetModelProperties() const {
+        return *fModelProperties;
+    }
+    
+    template <typename ModelProperties, typename... Args>
+    auto SetModelProperties(Args&&... args) {
+        static_assert(std::is_base_of_v<IModelProperties, ModelProperties>, "unusable type provided");
+        
+        fModelProperties = std::make_unique<ModelProperties>(std::forward<Args>(args)...);
+    }
+    
+    auto SetModelProperties(IMPSharedPtr ptr) {        
+        fModelProperties = IMPSharedPtr;
+    }
+    
+    template <class MediumType, typename... Args>
+    static auto CreateMedium(Args&&... args) {
+        static_assert(std::is_base_of_v<IMediumModel, MediumType>, "unusable type provided, needs to be derived from \"IMediumModel\"");
+        
+        return std::make_shared<MediumType>(std::forward<Args>(args)...);
+    }
+        
+    // factory methods for creation of nodes
+    template <class VolumeType, typename... Args>
+    static auto CreateNode(Args&&...args) {
+        static_assert(std::is_base_of_v<Volume, VolumeType>, "unusable type provided, needs to be derived from \"Volume\"");
+        
+        return std::make_unique<VolumeTreeNode<IModelProperties>>(std::make_unique<VolumeType>(std::forward<Args>(args)...));
+    }
+    
+private:
+    std::vector<VTNUPtr> fChildNodes;
+    std::vector<VolumeTreeNode<IModelProperties> const*> fExcludedNodes;
+    VolumeTreeNode<IModelProperties> const* fParentNode = nullptr;
+    VolUPtr fGeoVolume;
+    std::shared_ptr<IModelProperties> fModelProperties;    
+};
+
+}
+
+#endif
diff --git a/Framework/Geometry/CMakeLists.txt b/Framework/Geometry/CMakeLists.txt
index a4fc43cb3364881d7f3e42939b0985b69df4afab..d4aef5a42682b482596a2a347c247e5311bf6d37 100644
--- a/Framework/Geometry/CMakeLists.txt
+++ b/Framework/Geometry/CMakeLists.txt
@@ -9,6 +9,7 @@ set (
   Vector.h
   Point.h
   Sphere.h
+  Volume.h
   CoordinateSystem.h
   Helix.h
   BaseVector.h
diff --git a/Framework/Geometry/Sphere.h b/Framework/Geometry/Sphere.h
index 28b4e7dc63d5ded1ecd6d047df442af4557e4f20..76699690c5d49b40a7e25d4f44e95cbead64b5e0 100644
--- a/Framework/Geometry/Sphere.h
+++ b/Framework/Geometry/Sphere.h
@@ -1,23 +1,24 @@
 #ifndef _include_SPHERE_H_
 #define _include_SPHERE_H_
 
+#include <corsika/geometry/Volume.h>
 #include <corsika/geometry/Point.h>
 #include <corsika/units/PhysicalUnits.h>
 
 namespace corsika::geometry {
 
-  class Sphere {
-    Point center;
-    LengthType const radius;
+  class Sphere : public Volume {
+    Point const fCenter;
+    LengthType const fRadius;
 
   public:
     Sphere(Point const& pCenter, LengthType const pRadius)
-        : center(pCenter)
-        , radius(pRadius) {}
+        : fCenter(pCenter)
+        , fRadius(pRadius) {}
 
     //! returns true if the Point p is within the sphere
-    auto isInside(Point const& p) const {
-      return radius * radius > (center - p).squaredNorm();
+    bool Contains(Point const& p) const override {
+      return fRadius * fRadius > (fCenter - p).squaredNorm();
     }
   };
 
diff --git a/Framework/Geometry/Volume.h b/Framework/Geometry/Volume.h
new file mode 100644
index 0000000000000000000000000000000000000000..badbeb9df00ee72d77894ddd175b699e2fac19ae
--- /dev/null
+++ b/Framework/Geometry/Volume.h
@@ -0,0 +1,19 @@
+#ifndef _include_VOLUME_H_
+#define _include_VOLUME_H_
+
+#include <corsika/geometry/Point.h>
+
+namespace corsika::geometry {
+
+  class Volume {
+  
+  public:
+    //! returns true if the Point p is within the volume
+    virtual bool Contains(Point const& p) const = 0;
+    
+    virtual ~Volume() = default;
+  };
+
+} // namespace corsika::geometry
+
+#endif
diff --git a/Framework/Geometry/testGeometry.cc b/Framework/Geometry/testGeometry.cc
index 3c7f63af445c847541fd2c3fc665aa8ae4d3198d..440930718e9e1e6c5a92599ed1d1680988beab8c 100644
--- a/Framework/Geometry/testGeometry.cc
+++ b/Framework/Geometry/testGeometry.cc
@@ -116,8 +116,8 @@ TEST_CASE("Sphere") {
   Sphere sphere(center, 5_m);
 
   SECTION("isInside") {
-    REQUIRE_FALSE(sphere.isInside(Point(rootCS, {100_m, 0_m, 0_m})));
-    REQUIRE(sphere.isInside(Point(rootCS, {2_m, 3_m, 4_m})));
+    REQUIRE_FALSE(sphere.Contains(Point(rootCS, {100_m, 0_m, 0_m})));
+    REQUIRE(sphere.Contains(Point(rootCS, {2_m, 3_m, 4_m})));
   }
 }