In EventModeling, there is one pattern where everything really comes together. The State Change Pattern. At first glance, it sounds almost trivial:
An actor performs an activity that changes something in the system.
But once you look a bit closer, you realize that this simple statement carries almost everything we struggle with in real systems.
Let’s make it tangible. Take a familiar example. A student enrolls in a university course. It sounds straightforward. But it rarely is.
At the business level we start with a simple activity:
Student enrolls in course
Even at this level, questions immediately come up.
Who is actually allowed to do this? Under which conditions? And what exactly changes when the enrollment happens?
At this point, we are not talking about technology yet. We are simply trying to understand the business meaning of the activity.
It’s business rules that define the activity. The enrollment is only valid if certain rules hold. The course must not be full. The student must be registered. The student must not exceed the maximum number of courses. These are not edge cases or validations you add later. They are part of what the activity is. Without them, “enrollment” has no real meaning.
Let’s talk about Roles, permissions, and separation of duty (SoD). Here we move into control. The actor must have the role of a student. That role must be allowed to perform an enrollment. And we have to respect separation of duties. For example, a university administrator should not be able to enroll themselves as a student.
This is where things often start to get blurry in many systems. Responsibilities are mixed, permissions are implicit, and boundaries are not clearly enforced. A clean design does the opposite. It makes permissions explicit and enforces them right where the activity happens.
Auditability is not optional. Every enrollment needs to be traceable. Who did it? When did it happen? Under which conditions? What was the outcome? This is often seen as a compliance requirement. But in reality, it is much more than that. It is the foundation for trusting the system.
Business wants visibility. From a university’s perspective, it is not enough that enrollments work. They want to understand what is going on. How quickly do courses fill up? Where do enrollments fail? Are students blocked by certain rules? Are there unusual patterns? This is not system monitoring. This is business visibility.
What the State Change Pattern really implies is that if you model this properly, you are not just describing an activity. You are defining identity and access control, business rule enforcement, compliance boundaries, audit trails, observability, and APIs that align directly with business activities. All of that is embedded in this one pattern.
From model to implementation
If we take the model seriously, the implementation should reflect it.
One useful way to think about it is this: each business activity becomes one API endpoint.
Authentication can be handled by a system like Keycloak, where a JWT carries the roles of the actor. A central authority service resolves which permissions are granted to those roles. And each endpoint enforces exactly the permission that corresponds to the business activity.
Combined with a vertical slice architecture, each slice maps directly to one activity.
Example: API endpoint
POST /courses/{courseId}/enroll
Authorization: Bearer <JWT>
function enrollStudent(command, jwt) {
const roles = jwt.roles;
if (!permissionService.hasPermission(roles, "course.enroll")) {
throw new ForbiddenError();
}
const events = eventStore.query({
studentId: command.studentId,
courseId: command.courseId
});
if (isCourseFull(events)) {
throw new Error("Course is full");
}
if (!isStudentRegistered(events)) {
throw new Error("Student not registered");
}
if (exceedsMaxCourses(events)) {
throw new Error("Max courses exceeded");
}
const event = {
type: "StudentEnrolled",
studentId: command.studentId,
courseId: command.courseId,
timestamp: new Date(),
performedBy: jwt.sub
};
eventStore.append(event);
return event;
}
Turns out aggregate-less Event Sourcing is really powerful. Instead of relying on aggregates, we can directly filter the events we need.
With PostgreSQL, a GIN index on JSONB can make this surprisingly efficient.
CREATE TABLE event_store (
id UUID PRIMARY KEY,
type TEXT,
payload JSONB,
timestamp TIMESTAMP
);
CREATE INDEX idx_event_payload ON event_store USING GIN (payload);
And then query only the relevant events:
SELECT * FROM event_store
WHERE payload @> '{"studentId": "123"}'
AND payload @> '{"courseId": "456"}';
No aggregates. No reconstruction overhead. Just filtering the events that matter.
If we follow that path, observability comes for free. Once everything is captured as events, different needs can be served from the same source.
Audit becomes event history. Monitoring becomes event streams. Analytics becomes projections. One source, different perspectives.
The deeper insight is that the State Change Pattern is not just about changing state. It is about bringing several concerns together in one place: business intent, rules and constraints, permissions and compliance, technical implementation, and observability. When this is aligned, the system feels coherent. When it is not, you start to see gaps everywhere.
Most systems manage to implement the change itself. Far fewer manage to implement the meaning behind that change.
Originally published on LinkedIn (2026-05-03): https://www.linkedin.com/pulse/from-business-activity-system-truth-why-state-change-pattern-f8lte