DCB 4 - Vertical Slices

This example is another attempt at the “course subscriptions” challenge. This time we show another variation of the higher-level, more refactored style that we used in the previous example.

This implementation introduces the notion “slice” from the “vertical slices” style advocated by the event modelling community.

Each slice defines a tight consistency boundary that is specific to its use case. Events for a slice are selected according to its consistency boundary, and projected into a “current state” for a slice according to its own projector function. This produces a “current state” which is used sometimes as a “decision model” for slices that generate new events in application command methods, and in other cases to support returning values from application query methods.

Events

The “decision” events that the slices depend on are shown below. They all derive from the decision class.

class StudentJoinedCourse(Decision):
    student_id: StudentID
    course_id: CourseID
class StudentLeftCourse(Decision):
    student_id: StudentID
    course_id: CourseID
class StudentRegistered(Decision):
    student_id: StudentID
    name: str
    max_courses: int
class StudentNameUpdated(Decision):
    student_id: StudentID
    name: str
class StudentMaxCoursesUpdated(Decision):
    student_id: StudentID
    max_courses: int
class CourseRegistered(Decision):
    course_id: CourseID
    name: str
    places: int
class CourseNameUpdated(Decision):
    course_id: CourseID
    name: str
class CoursePlacesUpdated(Decision):
    course_id: CourseID
    places: int

Slices

Each slice shown below derives from the base class Slice.

The slices shown below are entirely independent of each other. They depend only on the “decision” event classes that are relevant to their use case. The big advantage of this style is that because slices define tight consistency boundaries according to their requirements, they will select only the events they actually require. And it naturally arises that there will be much less accidental overlap in the consistency boundaries between use cases. This means, in theory, there will be less contention between concurrent operations involving different use cases with the same tags. But more importantly, the decision models and the consistency boundaries are well-matched to each other and to the use case. That’s why this is a great style.

class RegisterStudent(Slice[Decision]):
    def __init__(self, name: str, max_courses: int):
        self.student_id = StudentID(f"student-{uuid4()}")
        self.name = name
        self.max_courses = max_courses

    def consistency_boundary(self) -> Selector:
        return Selector(types=[StudentRegistered], tags=[self.student_id])

    def execute(self) -> None:
        self.trigger_event(
            StudentRegistered,
            tags=[self.student_id],
            student_id=self.student_id,
            name=self.name,
            max_courses=self.max_courses,
        )
class UpdateStudentName(Slice[Decision]):
    def __init__(self, student_id: StudentID, name: str) -> None:
        self.id = student_id
        self.name = name
        self.student_was_registered: bool = False

    def consistency_boundary(self) -> Selector:
        return Selector(types=[StudentRegistered, StudentNameUpdated], tags=[self.id])

    @event(StudentRegistered)
    def _(self) -> None:
        self.student_was_registered = True

    def execute(self) -> None:
        assert self.student_was_registered
        self.trigger_event(
            StudentNameUpdated,
            tags=[self.id],
            student_id=self.id,
            name=self.name,
        )
class UpdateMaxCourses(Slice[Decision]):
    def __init__(self, student_id: StudentID, max_courses: int) -> None:
        self.student_was_registered: bool = False
        self.id = student_id
        self.max_courses = max_courses

    def consistency_boundary(self) -> Selector:
        return Selector(
            types=[StudentRegistered, StudentMaxCoursesUpdated],
            tags=[self.id],
        )

    @event(StudentRegistered)
    def _(self) -> None:
        self.student_was_registered = True

    def execute(self) -> None:
        assert self.student_was_registered
        self.trigger_event(
            StudentMaxCoursesUpdated,
            tags=[self.id],
            student_id=self.id,
            max_courses=self.max_courses,
        )
class RegisterCourse(Slice[Decision]):
    def __init__(self, name: str, places: int):
        self.course_id = CourseID(f"course-{uuid4()}")
        self.name = name
        self.places = places

    def consistency_boundary(self) -> Selector:
        return Selector(types=[CourseRegistered], tags=[self.course_id])

    def execute(self) -> None:
        self.trigger_event(
            CourseRegistered,
            tags=[self.course_id],
            course_id=self.course_id,
            name=self.name,
            places=self.places,
        )
class UpdateCourseName(Slice[Decision]):
    def __init__(self, course_id: CourseID, name: str) -> None:
        self.id = course_id
        self.name = name
        self.course_was_registered: bool = False

    def consistency_boundary(self) -> Selector:
        return Selector(types=[CourseRegistered, CourseNameUpdated], tags=[self.id])

    @event(CourseRegistered)
    def _(self) -> None:
        self.course_was_registered = True

    def execute(self) -> None:
        assert self.course_was_registered
        self.trigger_event(
            CourseNameUpdated,
            tags=[self.id],
            course_id=self.id,
            name=self.name,
        )
class UpdatePlaces(Slice[Decision]):
    def __init__(self, course_id: CourseID, places: int) -> None:
        self.id = course_id
        self.places = places
        self.course_was_registered: bool = False

    def consistency_boundary(self) -> Selector:
        return Selector(types=[CourseRegistered, CoursePlacesUpdated], tags=[self.id])

    @event(CourseRegistered)
    def _(self) -> None:
        self.course_was_registered = True

    def execute(self) -> None:
        assert self.course_was_registered
        self.trigger_event(
            CoursePlacesUpdated,
            tags=[self.id],
            course_id=self.id,
            places=self.places,
        )
class StudentJoinsCourse(Slice[Decision]):
    def __init__(self, student_id: StudentID, course_id: CourseID) -> None:
        self.student_id = student_id
        self.course_id = course_id
        self.course_was_registered = False
        self.student_was_registered = False
        self.student_max_courses = 0
        self.course_places = 0
        self.students_on_course: list[StudentID] = []
        self.courses_for_student: list[CourseID] = []

    def consistency_boundary(self) -> list[Selector]:
        return [
            Selector(
                types=[
                    StudentRegistered,
                    StudentMaxCoursesUpdated,
                    StudentJoinedCourse,
                    StudentLeftCourse,
                ],
                tags=[self.student_id],
            ),
            Selector(
                types=[
                    CourseRegistered,
                    CoursePlacesUpdated,
                    StudentJoinedCourse,
                    StudentLeftCourse,
                ],
                tags=[self.course_id],
            ),
        ]

    @event(StudentRegistered)
    def _(self, max_courses: int) -> None:
        self.student_was_registered = True
        self.student_max_courses = max_courses

    @event(CourseRegistered)
    def _(self, places: int) -> None:
        self.course_was_registered = True
        self.course_places = places

    @event(StudentJoinedCourse)
    def _(self, student_id: StudentID, course_id: CourseID) -> None:
        if student_id == self.student_id:
            self.courses_for_student.append(course_id)
        if course_id == self.course_id:
            self.students_on_course.append(student_id)

    @event(StudentLeftCourse)
    def _(self, student_id: StudentID, course_id: CourseID) -> None:
        if student_id == self.student_id:
            self.courses_for_student.remove(course_id)
        if course_id == self.course_id:
            self.students_on_course.remove(student_id)

    @event(StudentMaxCoursesUpdated)
    def _(self, max_courses: int) -> None:
        self.student_max_courses = max_courses

    @event(CoursePlacesUpdated)
    def _(self, places: int) -> None:
        self.course_places = places

    def execute(self) -> None:
        if not self.course_was_registered:
            raise CourseNotFoundError(self.course_id)
        if not self.student_was_registered:
            raise StudentNotFoundError(self.student_id)
        if len(self.students_on_course) >= self.course_places:
            raise FullyBookedError(self.course_id)
        if len(self.courses_for_student) >= self.student_max_courses:
            raise TooManyCoursesError(self.student_id)
        if self.student_id in self.students_on_course:
            raise AlreadyJoinedError((self.student_id, self.course_id))
        self.trigger_event(
            StudentJoinedCourse,
            tags=[self.student_id, self.course_id],
            student_id=self.student_id,
            course_id=self.course_id,
        )
class StudentLeavesCourse(Slice[Decision]):
    def __init__(self, student_id: StudentID, course_id: CourseID) -> None:
        self.student_id = student_id
        self.course_id = course_id
        self.course_was_registered = False
        self.student_was_registered = False
        self.students_on_course: list[StudentID] = []
        self.courses_for_student: list[CourseID] = []

    def consistency_boundary(self) -> list[Selector]:
        return [
            Selector(
                types=[StudentRegistered, StudentJoinedCourse, StudentLeftCourse],
                tags=[self.student_id],
            ),
            Selector(
                types=[CourseRegistered, StudentJoinedCourse, StudentLeftCourse],
                tags=[self.course_id],
            ),
        ]

    @event(StudentRegistered)
    def _(self) -> None:
        self.student_was_registered = True

    @event(CourseRegistered)
    def _(self) -> None:
        self.course_was_registered = True

    @event(StudentJoinedCourse)
    def _(self, student_id: StudentID, course_id: CourseID) -> None:
        if student_id == self.student_id:
            self.courses_for_student.append(course_id)
        if course_id == self.course_id:
            self.students_on_course.append(student_id)

    @event(StudentLeftCourse)
    def _(self, student_id: StudentID, course_id: CourseID) -> None:
        if student_id == self.student_id:
            self.courses_for_student.remove(course_id)
        if course_id == self.course_id:
            self.students_on_course.remove(student_id)

    def execute(self) -> None:
        if not self.course_was_registered:
            raise CourseNotFoundError
        if not self.student_was_registered:
            raise StudentNotFoundError
        if self.student_id not in self.students_on_course:
            raise NotAlreadyJoinedError
        self.trigger_event(
            StudentLeftCourse,
            tags=[self.student_id, self.course_id],
            student_id=self.student_id,
            course_id=self.course_id,
        )
class StudentsIDs(Slice[Decision]):
    def __init__(self, course_id: CourseID) -> None:
        self.course_id = course_id
        self.student_ids: list[StudentID] = []

    def consistency_boundary(self) -> Selector:
        return Selector(types=type(self).projected_types, tags=[self.course_id])

    @event(StudentJoinedCourse)
    def _(self, student_id: StudentID) -> None:
        self.student_ids.append(student_id)

    @event(StudentLeftCourse)
    def _(self, student_id: StudentID) -> None:
        self.student_ids.remove(student_id)
class StudentNames(Slice[Decision]):
    def __init__(self, student_ids: list[StudentID]) -> None:
        self.student_id_names: dict[StudentID, str | None] = dict.fromkeys(
            student_ids, None
        )

    def consistency_boundary(self) -> list[Selector]:
        return [
            Selector(types=type(self).projected_types, tags=[student_id])
            for student_id in self.student_id_names
        ]

    @event(StudentRegistered)
    def _(self, student_id: StudentID, name: str) -> None:
        self.student_id_names[student_id] = name

    @event(StudentNameUpdated)
    def _(self, student_id: StudentID, name: str) -> None:
        self.student_id_names[student_id] = name

    @property
    def names(self) -> list[str]:
        return [n for n in self.student_id_names.values() if n]
class CourseIDs(Slice[Decision]):
    def __init__(self, student_id: StudentID) -> None:
        self.student_id = student_id
        self.course_ids: list[CourseID] = []

    def consistency_boundary(self) -> Selector:
        return Selector(types=type(self).projected_types, tags=[self.student_id])

    @event(StudentJoinedCourse)
    def _(self, course_id: CourseID) -> None:
        self.course_ids.append(course_id)

    @event(StudentLeftCourse)
    def _(self, course_id: CourseID) -> None:
        self.course_ids.remove(course_id)
class CourseNames(Slice[Decision]):
    def __init__(self, course_ids: list[CourseID]) -> None:
        self.course_id_names: dict[CourseID, str | None] = dict.fromkeys(
            course_ids, None
        )

    def consistency_boundary(self) -> list[Selector]:
        return [
            Selector(types=type(self).projected_types, tags=[student_id])
            for student_id in self.course_id_names
        ]

    @event(CourseRegistered)
    def _(self, course_id: CourseID, name: str) -> None:
        self.course_id_names[course_id] = name

    @event(CourseNameUpdated)
    def _(self, course_id: CourseID, name: str) -> None:
        self.course_id_names[course_id] = name

    @property
    def names(self) -> list[str]:
        return [n for n in self.course_id_names.values() if n]
class Student(Slice[Decision]):
    def __init__(self, student_id: StudentID) -> None:
        self.student_was_registered: bool = False
        self.id = student_id
        self.name: str = ""
        self.max_courses: int = 0
        self.course_ids: list[CourseID] = []

    def consistency_boundary(self) -> Selector:
        return Selector(tags=[self.id])

    @event(StudentRegistered)
    def _(self, name: str, max_courses: int) -> None:
        self.student_was_registered = True
        self.name = name
        self.max_courses = max_courses

    @event(StudentNameUpdated)
    def _(self, name: str) -> None:
        self.name = name

    @event(StudentMaxCoursesUpdated)
    def _(self, max_courses: int) -> None:
        self.max_courses = max_courses

    @event(StudentJoinedCourse)
    def _(self, course_id: CourseID) -> None:
        self.course_ids.append(course_id)

    @event(StudentLeftCourse)
    def _(self, course_id: CourseID) -> None:
        self.course_ids.remove(course_id)
class Course(Slice[Decision]):
    def __init__(self, course_id: CourseID) -> None:
        self.course_was_registered: bool = False
        self.id = course_id
        self.name: str = ""
        self.places = 0
        self.student_ids: list[StudentID] = []

    def consistency_boundary(self) -> Selector:
        return Selector(tags=[self.id])

    @event(CourseRegistered)
    def _(self, name: str, places: int) -> None:
        self.course_was_registered = True
        self.name = name
        self.places = places

    @event(CourseNameUpdated)
    def _(self, name: str) -> None:
        self.name = name

    @event(CoursePlacesUpdated)
    def _(self, places: int) -> None:
        self.places = places

    @event(StudentJoinedCourse)
    def _(self, student_id: StudentID) -> None:
        self.student_ids.append(student_id)

    @event(StudentLeftCourse)
    def _(self, student_id: StudentID) -> None:
        self.student_ids.remove(student_id)

Application

The nice thing about this implementation is that the application command methods have all become straightforward one-liners that simply allow the domain model defined by the slices to enjoy the persistence infrastructure provided by the application. This happens naturally by pushing into the slices all responsibilities for selecting events, for projecting events into a decision model, and for generating new events, keeping their differences encapsulated behind a standard interface.

class EnrolmentWithVerticalSlices(DCBApplication, EnrolmentInterface):
    env: Mapping[str, str] = {
        "MAPPER_TOPIC": get_topic(MessagePackMapper),
        **DCBApplication.env,
    }

    def register_student(self, name: str, max_courses: int) -> StudentID:
        return self.do(RegisterStudent(name, max_courses)).student_id

    def register_course(self, name: str, places: int) -> CourseID:
        return self.do(RegisterCourse(name, places)).course_id

    def join_course(self, student_id: StudentID, course_id: CourseID) -> None:
        self.do(StudentJoinsCourse(student_id, course_id))

    def leave_course(self, student_id: StudentID, course_id: CourseID) -> None:
        self.do(StudentLeavesCourse(student_id, course_id))

    def list_students_for_course(self, course_id: CourseID) -> list[str]:
        return self.do(StudentNames(self.do(StudentsIDs(course_id)).student_ids)).names

    def list_courses_for_student(self, student_id: StudentID) -> list[str]:
        return self.do(CourseNames(self.do(CourseIDs(student_id)).course_ids)).names

    def update_student_name(self, student_id: StudentID, name: str) -> None:
        self.do(UpdateStudentName(student_id, name))

    def update_max_courses(self, student_id: StudentID, max_courses: int) -> None:
        self.do(UpdateMaxCourses(student_id, max_courses))

    def update_course_name(self, course_id: CourseID, name: str) -> None:
        self.do(UpdateCourseName(course_id, name))

    def update_places(self, course_id: CourseID, places: int) -> None:
        self.do(UpdatePlaces(course_id, places))

    def get_student(self, student_id: StudentID) -> Student:
        return self.do(Student(student_id=student_id))

    def get_course(self, course_id: CourseID) -> Course:
        return self.do(Course(course_id=course_id))

Test case

The enrolment test case is extended to check EnrolmentWithVerticalSlices.

It has some extra steps to cover the extra methods that we have implemented, such as a student leaving a course, changes of name, changes to the number of places on a course and the “max courses” for student. The extra steps also show, a little bit, the non-conflicting nature of different slices.

class TestEnrolmentWithVerticalSlices(EnrolmentTestCase):
    def test_enrolment_in_memory(self) -> None:
        self.assert_implementation(EnrolmentWithVerticalSlices())

    def test_enrolment_with_postgres(self) -> None:
        env = {
            "PERSISTENCE_MODULE": "eventsourcing.dcb.postgres_tt",
            "POSTGRES_DBNAME": "eventsourcing",
            "POSTGRES_HOST": "127.0.0.1",
            "POSTGRES_PORT": "5432",
            "POSTGRES_USER": "eventsourcing",
            "POSTGRES_PASSWORD": "eventsourcing",
        }
        try:
            self.assert_implementation(EnrolmentWithVerticalSlices(env))
        finally:
            drop_tables()

    def test_enrolment_with_umadb(self) -> None:
        env = {
            "PERSISTENCE_MODULE": "eventsourcing_umadb",
            "UMADB_URI": "http://127.0.0.1:50051",
        }
        self.assert_implementation(EnrolmentWithVerticalSlices(env))

    def assert_implementation(self, app: EnrolmentInterface) -> None:
        super().assert_implementation(app)

        assert isinstance(app, EnrolmentWithVerticalSlices)
        # Register student.
        student_id = app.register_student(name="Max", max_courses=4)

        # Update name.
        app.update_student_name(student_id, "Maxine")
        student = app.get_student(student_id)
        self.assertEqual("Maxine", student.name)

        # Register course.
        course_id = app.register_course(name="Bio", places=3)

        # Update name.
        app.update_course_name(course_id, "Biology")
        course = app.get_course(course_id)
        self.assertEqual("Biology", course.name)

        # Join course.
        app.join_course(student_id=student_id, course_id=course_id)
        student = app.get_student(student_id)
        course = app.get_course(course_id)
        self.assertEqual([course_id], student.course_ids)
        self.assertEqual([student_id], course.student_ids)

        # List students for course.
        names = app.list_students_for_course(course_id)
        self.assertEqual(["Maxine"], names)

        # List courses for student.
        names = app.list_courses_for_student(student_id)
        self.assertEqual(["Biology"], names)

        # Leave course.
        app.leave_course(student_id=student_id, course_id=course_id)
        student = app.get_student(student_id)
        course = app.get_course(course_id)
        self.assertEqual([], student.course_ids)
        self.assertEqual([], course.student_ids)

        # Update max_courses for student.
        app.update_max_courses(student_id, 0)
        student = app.get_student(student_id)
        self.assertEqual(0, student.max_courses)

        # Update places for course.
        app.update_places(course_id, 0)
        course = app.get_course(course_id)
        self.assertEqual(0, course.places)

        # Check leaves course, updated max_courses, and places
        # events are effective when joining course.
        with self.assertRaises(FullyBookedError):
            app.join_course(student_id=student_id, course_id=course_id)

        # Increase places.
        app.update_places(course_id, 1)

        with self.assertRaises(TooManyCoursesError):
            app.join_course(student_id=student_id, course_id=course_id)

        # Increase max_courses.
        app.update_max_courses(student_id, 1)

        # Student can now rejoin course.
        app.join_course(student_id=student_id, course_id=course_id)

        # Check leaving a course doesn't conflict with concurrent name changes.
        leave = StudentLeavesCourse(student_id, course_id)
        app.repository.advance(leave)
        leave.execute()
        app.update_student_name(student_id, "Mollie")
        app.update_course_name(course_id, "Bio-science")
        app.repository.save(leave)

        # Check leaving a course doesn't conflict with concurrent name changes.
        join = StudentJoinsCourse(student_id, course_id)
        app.repository.advance(join)
        join.execute()
        app.update_student_name(student_id, "Millie")
        app.update_course_name(course_id, "Biological-science")
        app.repository.save(join)

        # Check leaving doesn't conflict with updating max_courses and places.
        leave = StudentLeavesCourse(student_id, course_id)
        app.repository.advance(leave)
        leave.execute()
        app.update_max_courses(student_id, 31)
        app.update_places(course_id, 28)
        app.repository.save(leave)

        # Check joining does conflict with updating max_courses and places.
        join = StudentJoinsCourse(student_id, course_id)
        app.repository.advance(join)
        join.execute()
        app.update_max_courses(student_id, 39)
        app.update_places(course_id, 43)
        with self.assertRaises(IntegrityError):
            app.repository.save(join)

        # Check updating max_courses doesn't conflict with updating name.
        rename = UpdateStudentName(student_id, "Maddy")
        app.repository.advance(rename)
        rename.execute()
        app.update_max_courses(student_id, 101)
        app.repository.save(rename)

        max_courses = UpdateMaxCourses(student_id, 50)
        app.repository.advance(max_courses)
        max_courses.execute()
        app.update_student_name(student_id, "Mandy")
        app.repository.save(max_courses)

        student = app.get_student(student_id)
        self.assertEqual("Mandy", student.name)
        self.assertEqual(50, student.max_courses)
        self.assertEqual([], student.course_ids)

        # Can't call non-command underscore methods.
        with self.assertRaisesRegex(ProgrammingError, "cannot be used"):
            student._()  # type: ignore[call-arg]

Discussion

It must be said, that this is great style. We think “slices” and “dynamic consistency boundaries” really go very well together, and we hope you agree.

To be fair, as we saw with the shopping cart example, “vertical slices” make the code altogether slightly repetitive, relatively verbose, and relatively complicated. Whilst some may enjoy the absolute separation between use cases, others may find “vertical slices” harder to reason about than event-sourced aggregates and the “enduring objects” that were discussed in the previous example. Certainly, unlike the event-sourced aggregates, these slices do not all fit on one screen. The “decision” events that naturally fall into sequences under one tag are not coherently encapsulated in a single entity, and there are many different consistency boundaries. For these reasons, there will tend to be a greater need for careful thought, and longer tests, without which this style seems to invite a greater likelihood of unseen programming errors.

When considering contention, we must remember that compared with, for example, event-sourced aggregates which use a simpler and therefore faster persistence model, DCB read and append operations are inherently more complex and therefore slower. As a result, although with “vertical slices” we can more easily code for tighter consistency boundaries, due to the slower operations of DCB, there will, in theory, be more risk of roughly contemporary operations actually happening concurrently, causing contention where contention would not have occurred. But of course, there will be no conflict between operations involving sequences for different continuity IDs, in DCB, with or without “slices”, and with or without event-sourced aggregates, and so this is a relatively marginal consideration in most cases.

Whatever the relative merits, above all, we value having and supporting different styles and persistence models for event sourcing. And DCB with “vertical slices” is indeed a very commendable style.

Code reference

class examples.dcb_enrolment_with_vertical_slices.application.StudentJoinedCourse(student_id: StudentID, course_id: CourseID)[source]

Bases: Decision

student_id: StudentID
course_id: CourseID
class examples.dcb_enrolment_with_vertical_slices.application.StudentLeftCourse(student_id: StudentID, course_id: CourseID)[source]

Bases: Decision

student_id: StudentID
course_id: CourseID
class examples.dcb_enrolment_with_vertical_slices.application.StudentRegistered(student_id: StudentID, name: str, max_courses: int)[source]

Bases: Decision

student_id: StudentID
name: str
max_courses: int
class examples.dcb_enrolment_with_vertical_slices.application.StudentNameUpdated(student_id: StudentID, name: str)[source]

Bases: Decision

student_id: StudentID
name: str
class examples.dcb_enrolment_with_vertical_slices.application.StudentMaxCoursesUpdated(student_id: StudentID, max_courses: int)[source]

Bases: Decision

student_id: StudentID
max_courses: int
class examples.dcb_enrolment_with_vertical_slices.application.CourseRegistered(course_id: CourseID, name: str, places: int)[source]

Bases: Decision

course_id: CourseID
name: str
places: int
class examples.dcb_enrolment_with_vertical_slices.application.CourseNameUpdated(course_id: CourseID, name: str)[source]

Bases: Decision

course_id: CourseID
name: str
class examples.dcb_enrolment_with_vertical_slices.application.CoursePlacesUpdated(course_id: CourseID, places: int)[source]

Bases: Decision

course_id: CourseID
places: int
class examples.dcb_enrolment_with_vertical_slices.application.RegisterStudent(*_: Any, **__: Any)[source]

Bases: Slice[Decision]

__init__(name: str, max_courses: int)[source]
consistency_boundary() Selector[source]
execute() None[source]
do_projection = False
projected_types = []
last_known_position: int | None
new_decisions: list[Tagged[TDecision]]
class examples.dcb_enrolment_with_vertical_slices.application.UpdateStudentName(*_: Any, **__: Any)[source]

Bases: Slice[Decision]

__init__(student_id: StudentID, name: str) None[source]
consistency_boundary() Selector[source]
execute() None[source]
do_projection = True
projected_types = [<class 'examples.dcb_enrolment_with_vertical_slices.application.StudentRegistered'>]
last_known_position: int | None
new_decisions: list[Tagged[TDecision]]
class examples.dcb_enrolment_with_vertical_slices.application.UpdateMaxCourses(*_: Any, **__: Any)[source]

Bases: Slice[Decision]

__init__(student_id: StudentID, max_courses: int) None[source]
consistency_boundary() Selector[source]
execute() None[source]
do_projection = True
projected_types = [<class 'examples.dcb_enrolment_with_vertical_slices.application.StudentRegistered'>]
last_known_position: int | None
new_decisions: list[Tagged[TDecision]]
class examples.dcb_enrolment_with_vertical_slices.application.RegisterCourse(*_: Any, **__: Any)[source]

Bases: Slice[Decision]

__init__(name: str, places: int)[source]
consistency_boundary() Selector[source]
execute() None[source]
do_projection = False
projected_types = []
last_known_position: int | None
new_decisions: list[Tagged[TDecision]]
class examples.dcb_enrolment_with_vertical_slices.application.UpdateCourseName(*_: Any, **__: Any)[source]

Bases: Slice[Decision]

__init__(course_id: CourseID, name: str) None[source]
consistency_boundary() Selector[source]
execute() None[source]
do_projection = True
projected_types = [<class 'examples.dcb_enrolment_with_vertical_slices.application.CourseRegistered'>]
last_known_position: int | None
new_decisions: list[Tagged[TDecision]]
class examples.dcb_enrolment_with_vertical_slices.application.UpdatePlaces(*_: Any, **__: Any)[source]

Bases: Slice[Decision]

__init__(course_id: CourseID, places: int) None[source]
consistency_boundary() Selector[source]
execute() None[source]
do_projection = True
projected_types = [<class 'examples.dcb_enrolment_with_vertical_slices.application.CourseRegistered'>]
last_known_position: int | None
new_decisions: list[Tagged[TDecision]]
class examples.dcb_enrolment_with_vertical_slices.application.StudentJoinsCourse(*_: Any, **__: Any)[source]

Bases: Slice[Decision]

__init__(student_id: StudentID, course_id: CourseID) None[source]
consistency_boundary() list[Selector][source]
execute() None[source]
do_projection = True
projected_types = [<class 'examples.dcb_enrolment_with_vertical_slices.application.StudentRegistered'>, <class 'examples.dcb_enrolment_with_vertical_slices.application.CourseRegistered'>, <class 'examples.dcb_enrolment_with_vertical_slices.application.StudentJoinedCourse'>, <class 'examples.dcb_enrolment_with_vertical_slices.application.StudentLeftCourse'>, <class 'examples.dcb_enrolment_with_vertical_slices.application.StudentMaxCoursesUpdated'>, <class 'examples.dcb_enrolment_with_vertical_slices.application.CoursePlacesUpdated'>]
last_known_position: int | None
new_decisions: list[Tagged[TDecision]]
class examples.dcb_enrolment_with_vertical_slices.application.StudentLeavesCourse(*_: Any, **__: Any)[source]

Bases: Slice[Decision]

__init__(student_id: StudentID, course_id: CourseID) None[source]
consistency_boundary() list[Selector][source]
execute() None[source]
do_projection = True
projected_types = [<class 'examples.dcb_enrolment_with_vertical_slices.application.StudentRegistered'>, <class 'examples.dcb_enrolment_with_vertical_slices.application.CourseRegistered'>, <class 'examples.dcb_enrolment_with_vertical_slices.application.StudentJoinedCourse'>, <class 'examples.dcb_enrolment_with_vertical_slices.application.StudentLeftCourse'>]
last_known_position: int | None
new_decisions: list[Tagged[TDecision]]
class examples.dcb_enrolment_with_vertical_slices.application.StudentsIDs(*_: Any, **__: Any)[source]

Bases: Slice[Decision]

__init__(course_id: CourseID) None[source]
consistency_boundary() Selector[source]
do_projection = True
projected_types = [<class 'examples.dcb_enrolment_with_vertical_slices.application.StudentJoinedCourse'>, <class 'examples.dcb_enrolment_with_vertical_slices.application.StudentLeftCourse'>]
last_known_position: int | None
new_decisions: list[Tagged[TDecision]]
class examples.dcb_enrolment_with_vertical_slices.application.StudentNames(*_: Any, **__: Any)[source]

Bases: Slice[Decision]

__init__(student_ids: list[StudentID]) None[source]
consistency_boundary() list[Selector][source]
property names: list[str]
do_projection = True
projected_types = [<class 'examples.dcb_enrolment_with_vertical_slices.application.StudentRegistered'>, <class 'examples.dcb_enrolment_with_vertical_slices.application.StudentNameUpdated'>]
last_known_position: int | None
new_decisions: list[Tagged[TDecision]]
class examples.dcb_enrolment_with_vertical_slices.application.CourseIDs(*_: Any, **__: Any)[source]

Bases: Slice[Decision]

__init__(student_id: StudentID) None[source]
consistency_boundary() Selector[source]
do_projection = True
projected_types = [<class 'examples.dcb_enrolment_with_vertical_slices.application.StudentJoinedCourse'>, <class 'examples.dcb_enrolment_with_vertical_slices.application.StudentLeftCourse'>]
last_known_position: int | None
new_decisions: list[Tagged[TDecision]]
class examples.dcb_enrolment_with_vertical_slices.application.CourseNames(*_: Any, **__: Any)[source]

Bases: Slice[Decision]

__init__(course_ids: list[CourseID]) None[source]
consistency_boundary() list[Selector][source]
property names: list[str]
do_projection = True
projected_types = [<class 'examples.dcb_enrolment_with_vertical_slices.application.CourseRegistered'>, <class 'examples.dcb_enrolment_with_vertical_slices.application.CourseNameUpdated'>]
last_known_position: int | None
new_decisions: list[Tagged[TDecision]]
class examples.dcb_enrolment_with_vertical_slices.application.Student(*_: Any, **__: Any)[source]

Bases: Slice[Decision]

__init__(student_id: StudentID) None[source]
consistency_boundary() Selector[source]
do_projection = True
projected_types = [<class 'examples.dcb_enrolment_with_vertical_slices.application.StudentRegistered'>, <class 'examples.dcb_enrolment_with_vertical_slices.application.StudentNameUpdated'>, <class 'examples.dcb_enrolment_with_vertical_slices.application.StudentMaxCoursesUpdated'>, <class 'examples.dcb_enrolment_with_vertical_slices.application.StudentJoinedCourse'>, <class 'examples.dcb_enrolment_with_vertical_slices.application.StudentLeftCourse'>]
last_known_position: int | None
new_decisions: list[Tagged[TDecision]]
class examples.dcb_enrolment_with_vertical_slices.application.Course(*_: Any, **__: Any)[source]

Bases: Slice[Decision]

__init__(course_id: CourseID) None[source]
consistency_boundary() Selector[source]
do_projection = True
projected_types = [<class 'examples.dcb_enrolment_with_vertical_slices.application.CourseRegistered'>, <class 'examples.dcb_enrolment_with_vertical_slices.application.CourseNameUpdated'>, <class 'examples.dcb_enrolment_with_vertical_slices.application.CoursePlacesUpdated'>, <class 'examples.dcb_enrolment_with_vertical_slices.application.StudentJoinedCourse'>, <class 'examples.dcb_enrolment_with_vertical_slices.application.StudentLeftCourse'>]
last_known_position: int | None
new_decisions: list[Tagged[TDecision]]
class examples.dcb_enrolment_with_vertical_slices.application.EnrolmentWithVerticalSlices(env: Mapping[str, str] | None = None)[source]

Bases: DCBApplication, EnrolmentInterface

name = 'EnrolmentWithVerticalSlices'
env: Mapping[str, str] = {'MAPPER_TOPIC': 'eventsourcing.dcb.msgpack:MessagePackMapper', 'PERSISTENCE_MODULE': 'eventsourcing.dcb.popo'}
register_student(name: str, max_courses: int) StudentID[source]

Register a new student.

register_course(name: str, places: int) CourseID[source]

Register a new course.

join_course(student_id: StudentID, course_id: CourseID) None[source]

Enrol a student on a course.

leave_course(student_id: StudentID, course_id: CourseID) None[source]
list_students_for_course(course_id: CourseID) list[str][source]

List students enrolled on a course.

list_courses_for_student(student_id: StudentID) list[str][source]

List courses enrolled by a student.

update_student_name(student_id: StudentID, name: str) None[source]
update_max_courses(student_id: StudentID, max_courses: int) None[source]
update_course_name(course_id: CourseID, name: str) None[source]
update_places(course_id: CourseID, places: int) None[source]
get_student(student_id: StudentID) Student[source]
get_course(course_id: CourseID) Course[source]