Gabriel Schenker

Gabriel Schenker

One slice, end to end

One slice, end to end

When I analyse the code of a legacy application, or of an application built with hexagonal architecture, I have a hard time finding the logic that belongs to a business feature such as cancelling a policy. I have to open 14 files and I'm completely lost. A non-technical person has no chance of understanding the code. Even with AI, we spend a lot of tokens just to get a clear picture of how the feature is implemented. The business rules can hardly be mapped directly to code.

In Slices, not services I argued that vertical slices fix this. This time I want to show it with one feature, traced from the model to the code: cancelling a life policy during its cooling-off period.

The feature in the model

In an Event Model, this feature is one state-change slice, one of the four building blocks I described in EventModeling building blocks. It has three parts:

  • A screen, "Cancel policy", that shows the policy number and the date the cooling-off period ends, with a "Cancel" button.
  • A command, CancelPolicyInCoolingOff, with the policy, an optional reason, the time of the request, and the cooling-off end the screen showed.
  • An event, PolicyCancelledInCoolingOff, the fact that the policy was cancelled, with the time of the request.
The CancelPolicyInCoolingOff slice in an Event Modeling tool, top to bottom: the Cancel policy screen, the command CancelPolicyInCoolingOff with policyId, requestedAt, reason and coolingOffEndsAt, the event PolicyCancelledInCoolingOff with policyId, requestedAt and reason, and a spec lane with four specifications

The slice, modelled with Martin Dilger's agentic Event Modeling tool, app.eventmodelers.ai. The picture shows four of the specs; the plain success case and the mismatch case aren't shown.

The decision needs three facts from the past: was the policy issued, when does its cooling-off period end, and was it already cancelled? Four rules follow, and a business expert can read every one of them:

  1. The policy must exist. Otherwise: PolicyNotFound.
  2. It must not already be cancelled. Otherwise: PolicyAlreadyCancelled.
  3. The deadline on the request must match the one recorded when the policy was issued. Otherwise: CoolingOffEndMismatch.
  4. The request must arrive on or before the last moment of the cooling-off period. Otherwise: CoolingOffPeriodExpired.

The command carries the deadline the customer saw on the screen, but the decision always uses the deadline recorded when the policy was issued, so no caller can move it. If the two differ, the command is rejected with CoolingOffEndMismatch, so nobody cancels on the strength of a date they weren't shown.

In practice, a mismatch means the screen was out of date or had been tampered with. The request is refused and nothing is written.

The scenarios say what correct means

Before anyone writes code, the slice gets its scenarios. Each one has a given (past events), a when (the command) and a then (the new event or an error). For a policy whose cooling-off period ends on 30 November at 23:59:

  • Cancel on 10 November, and the policy is cancelled.
  • Cancel at 23:59 on 30 November, and the last moment still counts.
  • Cancel at 00:00 on 1 December, and the request is refused because the period has expired.
  • Cancel a policy that is already cancelled, and the request is refused.
  • Cancel a policy that doesn't exist, and the request is refused.
  • Cancel from a screen that showed a different deadline, and the request is refused.

These six scenarios are the specification. As I wrote in How we test just became the specification, they're also the tests.

One folder, one feature

Here's the code. The whole slice lives in one folder named after the command:

CancelPolicyInCoolingOff/
  CancelPolicyScreen.html
  CancelPolicyInCoolingOff.js
  CancelPolicyInCoolingOff.test.js

The screen is the screen. The handler file holds the decision. The test file holds the six scenarios. Outside the folder there's only shared glue: one file that connects the event store and one HTTP route.

The decision starts with its facts from the past. The library lets me declare them in a few lines:

import { queryItem } from 'es-dcb-library';

// Consistency boundary: the events this decision depends on.
export const decisionQuery = (policyId) =>
  queryItem({
    types: ['PolicyIssued', 'PolicyCancelledInCoolingOff'],
    tags: [`policy:${policyId}`],
  });

This is the model's "facts from the past", written as code. types says which facts: the policy was issued, or it was cancelled. tags says about which policy. The query matches events of any listed type that carry all the listed tags, and a business person can read it as easily as an engineer. The handler uses this one query twice, to load the facts and to protect them when it appends, so the boundary is declared in exactly one place.

The handler reads like the model:

export async function cancelPolicyInCoolingOff(store, { policyId, requestedAt, reason = null, coolingOffEndsAt }) {
  const query = decisionQuery(policyId);
  const { events, position } = await store.load(query);
  const state = decisionModel(events);

  if (!state.exists) throw new Error('PolicyNotFound');
  if (state.cancelled) throw new Error('PolicyAlreadyCancelled');
  // The command carries the deadline the screen showed; the recorded one decides.
  if (Date.parse(coolingOffEndsAt) !== Date.parse(state.coolingOffEndsAt)) throw new Error('CoolingOffEndMismatch');
  if (new Date(requestedAt) > new Date(state.coolingOffEndsAt)) throw new Error('CoolingOffPeriodExpired');

  const [event] = await store.append(
    {
      type: 'PolicyCancelledInCoolingOff',
      data: { policyId, requestedAt, reason },
      tags: [`policy:${policyId}`],
    },
    { failIfEventsMatch: query, after: position }, // fails if anything matching was written since the load
  );
  return event;
}

The four guard clauses are the four business rules, in the same order and with the same names. decisionQuery loads only the two event types this decision depends on, for this one policy. That is the Dynamic Consistency Boundary (DCB): the append fails if anything relevant changed since the load, so two cancellations can't both go through. There's no aggregate and no repository, and no service layer to pass through.

The tests read like the scenarios:

test('Last moment still counts', async () => {
  await given(PolicyIssued('P1'));
  await when({ policyId: 'P1', requestedAt: ENDS });
  await thenEvents('P1', [PolicyCancelledInCoolingOff('P1', ENDS)]);
});

The when helper fills in the deadline the screen showed, unless a scenario overrides it.

The code runs on my es-dcb-library and PostgreSQL. The six scenarios run as tests against a real PostgreSQL database, and all six pass.

What this changes

Go back to the 14 files. A business person who wants to know how cancellation works now opens one folder, and the rules they agreed on are right there, by name. An engineer new to the system finds the feature by its name. An AI agent reads three short files instead of tracing calls across layers, so it spends fewer tokens to understand the feature, and the scenarios tell it whether a change is correct.

The same holds for what's not in this slice. Refunding the premium is a separate automation slice that reacts to PolicyCancelledInCoolingOff. A regular cancellation outside the cooling-off period has different terms, so it's another slice too. Each gets its own folder, its own scenarios and its own small decision.

That's the one-to-one mapping I meant: one card in the model, one folder in the code, and one set of scenarios that both sides agreed on. When the model and the code have the same shape, nobody has to translate between them, whether that's a person or an agent.

Originally published on LinkedIn on 26 Oct 2026: https://www.linkedin.com/pulse/one-slice-end-gabriel-n-schenker-gomle/