diff --git a/src/data/graph.hpp b/src/data/graph.hpp
index 75271a18dd60006057266dffe390bb470a09d6b2..3d570985c8804e1809b5ebc1868df3c7b95b910c 100644
--- a/src/data/graph.hpp
+++ b/src/data/graph.hpp
@@ -19,11 +19,14 @@
 #include "nodes/user_point_config.hpp"
 
 #include "util/enums.hpp"
+#include "util/testing.hpp"
 
 #include <cassert>
 #include <tuple>
 #include <utility>
 
+class QTextStream;
+
 /**
  * \brief If N has a member function 'recalculate' and any of ChangedNodes is a
  * predecessor to N, returns true if 'recalculate' is declared to not throw any
@@ -72,6 +75,7 @@ struct DAG {
     StudentData mStudents{};
     BonusPoints mBonus{};
 
+private:
     PointsWithoutBonus<Closed> mClosedPointsWithoutBonus{mExam};
     PointsWithoutBonus<Open> mOpenPointsWithoutBonus{mExam};
     PointsWithoutBonus<Combined> mCombinedPointsWithoutBonus{mExam};
@@ -138,6 +142,7 @@ struct DAG {
             mClosedGrades, mOpenGrades, mGrades, mGradeAverage);
     }
 
+public:
     DAG(DynexiteExam exam, StudentData students, BonusPoints bonus,
         UserGradeConfig gradeConfig, UserPointConfig pointConfig)
         : mUserPointConfig(std::move(pointConfig)),
@@ -184,6 +189,12 @@ struct DAG {
             return mOpenPassingLimit.mMaxPassingLimit;
         }
     }
+    [[nodiscard]] constexpr auto getRelPassingLimit() const noexcept {
+        return mClosedPassingLimit.mRelPassingLimit;
+    }
+    [[nodiscard]] constexpr auto getAbsPassingLimit() const noexcept {
+        return mClosedPassingLimit.mAbsPassingLimit;
+    }
     [[nodiscard]] constexpr const auto &
     getUserGradeBoundaries(const GradingType grading) const noexcept {
         switch (grading) {
@@ -196,6 +207,18 @@ struct DAG {
             return mOpenUserBoundaries.mBoundaries;
         }
     }
+    [[nodiscard]] constexpr auto
+    getUserGradeBoundarySetAuto(const GradingType grading) const noexcept {
+        switch (grading) {
+        default:
+            assert(false);
+            [[fallthrough]];
+        case Closed:
+            return mClosedUserBoundaries.mSetAutomatically;
+        case Open:
+            return mOpenUserBoundaries.mSetAutomatically;
+        }
+    }
     [[nodiscard]] constexpr const auto &
     getGradeList(const GradingType grading) const noexcept {
         switch (grading) {
@@ -238,6 +261,12 @@ struct DAG {
             return mCombinedPointsWithoutBonus.mMaxPoints;
         }
     }
+    [[nodiscard]] constexpr auto getAvgFirstTryPoints() const noexcept {
+
+        return mUserPointConfig.mCountZerothAttemptAsFirstAttempt
+                   ? mFirstTry.mAvgFirstAndZerothTryPoints
+                   : mFirstTry.mAvgFirstTryPoints;
+    }
     [[nodiscard]] constexpr auto
     getOriginalMaxPointsBoundary(const GradingType grading) const noexcept {
         switch (grading) {
@@ -307,4 +336,14 @@ struct DAG {
     [[nodiscard]] auto hasBonus() const noexcept {
         return !mBonus.mTotalBonusPointsPerStudent.empty();
     }
+
+    [[nodiscard]] auto getStats() const noexcept { return mStats; }
+
+    [[nodiscard]] auto getGradeAverages() const noexcept {
+        return mGradeAverage;
+    }
+
+    friend class DataManager;
+    friend void writeCurrentSettingsToD2r(QTextStream &);
+    FRIEND_TEST(FirstTryDagTest, CorrectlyCalculatedInDag);
 };
diff --git a/src/file_io/d2r_parser.cpp b/src/file_io/d2r_parser.cpp
index dfe6a807e4654e52e2ce72f2326d886ca2ca21d6..ff9b0deb1c2a29ccb1dfd2b34ff4bcb512968342 100644
--- a/src/file_io/d2r_parser.cpp
+++ b/src/file_io/d2r_parser.cpp
@@ -384,6 +384,7 @@ QSharedPointer<ExamSave> parseD2r(QString d2rString, QWidget *parentWidget) {
     return readStringsToData(readData, parentWidget);
 }
 
+// Is a friend of struct DAG
 void writeCurrentSettingsToD2r(QTextStream &out) {
     const auto cur = DataManager::getCurrent();
     if (!cur)
@@ -442,12 +443,8 @@ void writeCurrentSettingsToD2r(QTextStream &out) {
         writeSingleSetting(out, QString("UserGradeBoundaries") + toString(type),
                            "");
         {
-            writeSingleSetting(
-                out, "SetAuto",
-                type == GradingType::Closed
-                    ? cur->mClosedUserBoundaries.mSetAutomatically
-                    : cur->mOpenUserBoundaries.mSetAutomatically,
-                true);
+            writeSingleSetting(out, "SetAuto",
+                               cur->getUserGradeBoundarySetAuto(type), true);
             for (const auto &g : grades) {
                 writeSingleSetting(
                     out, "Boundary" + QString::number(static_cast<int>(g)),
diff --git a/src/model/grade_model.cpp b/src/model/grade_model.cpp
index 73674f8cdb5f9c8e385b565d183c8980b3f75b8a..3e9bdfc417316ada97da2ebf8505a54ba30e0234 100644
--- a/src/model/grade_model.cpp
+++ b/src/model/grade_model.cpp
@@ -17,10 +17,7 @@ void GradeModel::updateData() {
 
         // count how many students have each respective grade
 
-        const QList<Grade> &gradeList =
-            mGradingType == GradingType::Closed ? cur->mClosedGrades.mGrades
-            : mGradingType == GradingType::Open ? cur->mOpenGrades.mGrades
-                                                : cur->mGrades.mGrades;
+        const auto &gradeList = cur->getGradeList(mGradingType);
 
         for (const auto &g : gradeList) {
             if (static_cast<int>(g) < mGradeCounts.size())
diff --git a/src/view/boundary_window/boundary_tab.cpp b/src/view/boundary_window/boundary_tab.cpp
index d1086680a5d8802a71399aa706660b804256e608..b3d9724113a8dfa80cc7af039433e7d7e9201856 100644
--- a/src/view/boundary_window/boundary_tab.cpp
+++ b/src/view/boundary_window/boundary_tab.cpp
@@ -220,9 +220,7 @@ void BoundaryTab::updateBoundaryTableValues(
         QSignalBlocker blocker(mAutoCheckBox);
 
         const auto autoChangeBoundaries =
-            mGrading == GradingType::Closed
-                ? cur->mClosedUserBoundaries.mSetAutomatically
-                : cur->mOpenUserBoundaries.mSetAutomatically;
+            cur->getUserGradeBoundarySetAuto(mGrading);
 
         mAutoCheckBox.setCheckState(autoChangeBoundaries
                                         ? Qt::CheckState::Checked
@@ -251,9 +249,7 @@ void BoundaryTab::updateBoundaryTableValues(
 
         // The current value of the spin-box,
         // which is the grade boundary for the current grade
-        const auto &val = mGrading == GradingType::Closed
-                              ? cur->mClosedUserBoundaries.mBoundaries[grade]
-                              : cur->mOpenUserBoundaries.mBoundaries[grade];
+        const auto &val = cur->getUserGradeBoundaries(mGrading)[grade];
 
         // String for the max number of points for the current grade,
         // equal to the next grade boundary minus one step, or the max amount of
@@ -264,11 +260,7 @@ void BoundaryTab::updateBoundaryTableValues(
             // Special case for grade 1.0: use max overall points as max points
             // of current grade boundary
             if (gradeIdx == 0) {
-                if (grading == GradingType::Closed) {
-                    res = cur->mChangedMaxPointsClosed.mChangedMaxPointBoundary;
-                } else {
-                    res = cur->mChangedMaxPointsOpen.mChangedMaxPointBoundary;
-                }
+                res = cur->getChangedMaxPointsBoundary(grading);
             } else {
                 // max is one step below the next boundary
                 // If that is below the current boundary, write "-" instead
@@ -289,12 +281,7 @@ void BoundaryTab::updateBoundaryTableValues(
         const auto &rangeMax = [cur, grading = mGrading, gradeIdx] {
             // Special case for grade 1.0
             if (gradeIdx == 0) {
-                if (grading == GradingType::Closed) {
-                    return cur->mChangedMaxPointsClosed
-                        .mChangedMaxPointBoundary;
-                } else {
-                    return cur->mChangedMaxPointsOpen.mChangedMaxPointBoundary;
-                }
+                return cur->getChangedMaxPointsBoundary(grading);
             } else {
                 return cur->getUserGradeBoundaries(
                     grading)[static_cast<Grade>(gradeIdx - 1)];
@@ -305,13 +292,8 @@ void BoundaryTab::updateBoundaryTableValues(
             if (gradeIdx == static_cast<std::size_t>(Grade::grade5_0)) {
                 return GradeBoundary{0};
             } else {
-                if (grading == GradingType::Closed) {
-                    return cur->mClosedUserBoundaries
-                        .mBoundaries[static_cast<Grade>(gradeIdx + 1)];
-                } else {
-                    return cur->mOpenUserBoundaries
-                        .mBoundaries[static_cast<Grade>(gradeIdx + 1)];
-                }
+                return cur->getUserGradeBoundaries(
+                    grading)[static_cast<Grade>(gradeIdx + 1)];
             }
         }();
 
diff --git a/src/view/main_window/export_rwthonline.cpp b/src/view/main_window/export_rwthonline.cpp
index 7fb660cdb39b6b8cce112afa50cce49e1797b8f6..cd5425b0bf6a9a7e13e19df5859b244e1a6afd69 100644
--- a/src/view/main_window/export_rwthonline.cpp
+++ b/src/view/main_window/export_rwthonline.cpp
@@ -40,10 +40,7 @@ qsizetype getColumn(const CsvLine &headerLine, const QString &headerText) {
     const auto nrBonusPoints = cur->mBonus.mTotalBonusPointsPerStudent
                                    .value(cur->mExam.mIdentifier[idx])
                                    .toString();
-    const auto maxGradeBoundary =
-        type == GradingType::Closed
-            ? cur->mChangedMaxPointsClosed.mChangedMaxPointBoundary
-            : cur->mChangedMaxPointsOpen.mChangedMaxPointBoundary;
+    const auto maxGradeBoundary = cur->getChangedMaxPointsBoundary(type);
     const auto max =
         maxGradeBoundary.toString(cur->mUserPointConfig.mGradeBoundaryStepSize);
 
@@ -68,11 +65,12 @@ qsizetype getColumn(const CsvLine &headerLine, const QString &headerText) {
     if (type == GradingType::Closed) {
         // Add info on average points of candidates taking the exam for the
         // first time
-        if (!cur->mFirstTry.mAvgFirstTryPoints.has_value()) {
+        const auto avgFirstTryPoints = cur->getAvgFirstTryPoints();
+        if (!avgFirstTryPoints.has_value()) {
             remark += str::fileExport::rwthOnlineRemarkAverageFirstNone;
         } else {
             remark += QString(str::fileExport::rwthOnlineRemarkAverageFirstAny)
-                          .arg(cur->mFirstTry.mAvgFirstTryPoints->toString());
+                          .arg(avgFirstTryPoints->toString());
         }
     }
 
diff --git a/src/view/main_window/grade_histogram.cpp b/src/view/main_window/grade_histogram.cpp
index e2ebbcc9552ecc5731e9364be3e4fd56fe1cbce7..2199272f94f752bab8751114fede4743bea0b00f 100644
--- a/src/view/main_window/grade_histogram.cpp
+++ b/src/view/main_window/grade_histogram.cpp
@@ -183,7 +183,7 @@ void GradeHistogram::gradesChanged(const EnumStorage<bool> &changedTypes) {
             // not attended
             mBarSets[modelIdx]->replace(
                 static_cast<int>(Grade::NR_VALS),
-                DataManager::getCurrent()->mStats.mNrRegisteredNotAttended);
+                DataManager::getCurrent()->getStats().mNrRegisteredNotAttended);
 
             // color non-passed-grades (5.0 and not attended) red by selecting
             // them and setting the selection-color to red
@@ -201,7 +201,7 @@ void GradeHistogram::updateYAxis() {
     // find largest bar (either the not-attended bar or the bar of a grade of a
     // grading type)
     const auto cur = DataManager::getCurrent();
-    int largestBar = cur ? cur->mStats.mNrRegisteredNotAttended : 0;
+    int largestBar = cur ? cur->getStats().mNrRegisteredNotAttended : 0;
     for (const auto &model : mModels) {
         largestBar = std::max(largestBar,
                               *std::ranges::max_element(model->mGradeCounts));
diff --git a/src/view/main_window/main_window.cpp b/src/view/main_window/main_window.cpp
index 40892fd7c4b5099cf1741af08e87eb898b1ec175..1e73dbe8eda0b588e301369c06147af657240f24 100644
--- a/src/view/main_window/main_window.cpp
+++ b/src/view/main_window/main_window.cpp
@@ -60,8 +60,8 @@ void printBoundaries(QString &str, const GradingType type, const bool hasType,
                " part:\n"
                "Grade --- [credits >=] --- [credits <]\n";
 
-        auto lastGradeBoundary =
-            cur->mChangedMaxPointsOpen.mChangedMaxPointBoundary;
+        // Initial value doesn't matter, because it gets overwritten with "max"
+        auto lastGradeBoundary = cur->getChangedMaxPointsBoundary(type);
 
         for (const auto g : grades) {
             const auto minBoundary = cur->getUserGradeBoundaries(type)[g];
@@ -378,7 +378,7 @@ void MainWindow::updateMenuItems() const {
     const auto cur = DataManager::getCurrent();
 
     const auto nrUnknownStudents =
-        cur ? cur->mStats.mNrAttendedNotRegistered : 0;
+        cur ? cur->getStats().mNrAttendedNotRegistered : 0;
 
     mExportUnknownStudentsAct->setText("Manual: Unknown Students (" +
                                        QString::number(nrUnknownStudents) +
diff --git a/src/view/main_window/min_max_point_screws.cpp b/src/view/main_window/min_max_point_screws.cpp
index f0fce86baf5c17ff9da398261a3f36b775c2d75a..d6a9d51019a909713bab8e69be945a7a5c905cc8 100644
--- a/src/view/main_window/min_max_point_screws.cpp
+++ b/src/view/main_window/min_max_point_screws.cpp
@@ -255,10 +255,10 @@ void MinMaxPointScrewColumn::updateHelpText() noexcept {
      */
     const GradeBoundaryFactory f{cur->mUserPointConfig.mGradeBoundaryStepSize,
                                  cur->mUserPointConfig.mGradeBoundaryRounding};
+
     const auto relPassMarkDecisive =
-        cur->mClosedPassingLimit.mRelPassingLimit.has_value() &&
-        f(*cur->mClosedPassingLimit.mRelPassingLimit) <
-            f(cur->mClosedPassingLimit.mAbsPassingLimit);
+        cur->getRelPassingLimit().has_value() &&
+        f(*cur->getRelPassingLimit()) < f(cur->getAbsPassingLimit());
 
     // Use rich text here for smart line breaks
     QString questionIconHoverText;
@@ -377,25 +377,27 @@ void MinMaxPointScrews::gradesChanged() noexcept {
         return;
     }
 
+    const auto gradeAverages = cur->getGradeAverages();
+
     // Number of decimal places for average grade is always 2, independent of
     // the grade boundary step size, because this is an averaged grade, not a
     // point count
-    if (cur->mGradeAverage.mAverageGrade.has_value()) {
+    if (gradeAverages.mAverageGrade.has_value()) {
         mAverageGrade.setText(
-            QString::number(*cur->mGradeAverage.mAverageGrade, 'f', 2));
+            QString::number(*gradeAverages.mAverageGrade, 'f', 2));
     } else {
         mAverageGrade.setText("-");
     }
-    if (cur->mGradeAverage.mAverageGradePassed.has_value()) {
+    if (gradeAverages.mAverageGradePassed.has_value()) {
         mAverageGradePassed.setText(
-            QString::number(*cur->mGradeAverage.mAverageGradePassed, 'f', 2));
+            QString::number(*gradeAverages.mAverageGradePassed, 'f', 2));
     } else {
         mAverageGradePassed.setText("-");
     }
 
     {
-        const auto &nrPassed = cur->mGradeAverage.mNrPassed;
-        const auto &nrAttended = static_cast<int>(cur->mGrades.mGrades.size());
+        const auto &nrPassed = gradeAverages.mNrPassed;
+        const auto &nrAttended = cur->getStats().mNrAttended;
         const auto &nrFailed = nrAttended - nrPassed;
         if (nrAttended == 0) {
             mNrPassed.setText(attendedResultNone);
diff --git a/src/view/statistic_window/calculation_window.cpp b/src/view/statistic_window/calculation_window.cpp
index 577040c6cf5a08110a7470d0d6311ee5f706db76..508821678217df1ee97cacc4193f96ba5780a58b 100644
--- a/src/view/statistic_window/calculation_window.cpp
+++ b/src/view/statistic_window/calculation_window.cpp
@@ -119,16 +119,13 @@ void CalculationWindow::gradesChanged() {
         return;
     }
 
-    const auto firstTryPoints =
-        cur->mUserPointConfig.mCountZerothAttemptAsFirstAttempt
-            ? cur->mFirstTry.mAvgFirstAndZerothTryPoints
-            : cur->mFirstTry.mAvgFirstTryPoints;
+    const auto firstTryPoints = cur->getAvgFirstTryPoints();
 
     const auto hasFirstTry = firstTryPoints.has_value();
     const auto stepSize = cur->mUserPointConfig.mGradeBoundaryStepSize;
 
     const auto changedMaxPoints =
-        cur->mChangedMaxPointsClosed.mChangedMaxPointBoundary;
+        cur->getChangedMaxPointsBoundary(GradingType::Closed);
     mMaxClosed.setText(changedMaxPoints.toString(stepSize));
     if (hasFirstTry) {
         mAvgFirstTry.setText(firstTryPoints->toStringLowPrecision());
@@ -136,14 +133,14 @@ void CalculationWindow::gradesChanged() {
         mAvgFirstTry.setText("None");
     }
 
-    const auto absPassMark = cur->mClosedPassingLimit.mAbsPassingLimit;
+    const auto absPassMark = cur->getAbsPassingLimit();
     mAbsPassMark.setText(QString("0.6 * %1 = <b>%2</b>")
                              .arg(changedMaxPoints.toString(stepSize),
                                   absPassMark.toStringLowPrecision()));
 
     // Relative pass mark
     const auto relPassMark0_5 =
-        toPoints(cur->mChangedMaxPointsClosed.mChangedMaxPointBoundary,
+        toPoints(changedMaxPoints,
                  cur->mUserPointConfig.mGradeBoundaryStepSize) *
         0.5;
     const auto relPassMark0_78 = firstTryPoints.value_or(Points{0.}) * 0.78;
diff --git a/src/view/statistic_window/statistic_window.cpp b/src/view/statistic_window/statistic_window.cpp
index 9aebea0a7490ce8deaa823af335095c0c11bf6db..ce1ab5eec3f0c14124cf8829df6253d1924775de 100644
--- a/src/view/statistic_window/statistic_window.cpp
+++ b/src/view/statistic_window/statistic_window.cpp
@@ -41,7 +41,7 @@ void StatisticWindow::nrStudentsChanged() {
         return;
     }
 
-    const auto &stats = cur->mStats;
+    const auto &stats = cur->getStats();
 
     mNrAttended.setText(QString::number(stats.mNrAttended));
     mNrAttendedRegistered.setText(QString::number(stats.mNrAttendedRegistered));
diff --git a/test/data/nodes/user_grade_boundaries_test.cpp b/test/data/nodes/user_grade_boundaries_test.cpp
index e06f1da76009a1466a0985c72957c71b5e97a51d..d1a4f1d914c4d7fa2f72532deb18a16204c866a5 100644
--- a/test/data/nodes/user_grade_boundaries_test.cpp
+++ b/test/data/nodes/user_grade_boundaries_test.cpp
@@ -60,8 +60,8 @@ TEST_P(UserGradeBoundaryTest, boundariesDescending) {
 TEST_P(UserGradeBoundaryTest, defaultFollowDefault) {
     const auto &graph = GetParam().second;
 
-    EXPECT_TRUE(graph.mClosedUserBoundaries.mSetAutomatically);
-    EXPECT_TRUE(graph.mOpenUserBoundaries.mSetAutomatically);
+    EXPECT_TRUE(graph.getUserGradeBoundarySetAuto(GradingType::Closed));
+    EXPECT_TRUE(graph.getUserGradeBoundarySetAuto(GradingType::Open));
 }
 
 // todo: split up?