Chapter 36: Boundaries as Application Geography
Applications have geography.
A click inside a billing settings page is different from a click inside onboarding. A save action inside a profile editor is different from a save action inside an admin impersonation flow. A delete button inside one table row belongs to a different entity than the same button in the next row. A button labelled Add Item in a checkout cart isn’t the same action as a button labelled Add Item in an inventory editor — even if the markup is identical.
The component model doesn’t have a vocabulary for this. A button is a button is a button. Where the button lives in the application’s logical structure is implicit, encoded in whatever the parent component happens to know and pass down through props.
Boundaries make the geography explicit. They’re the architectural piece that gives the capability modules from the previous chapter the context they need to interpret events. They’re also the architectural piece that the rest of Part III’s principles build on. Events bubble through boundaries (Chapter 23). Boundaries supply context to those events (this chapter). The capability modules from Chapter 35 receive events enriched with the surrounding context, without the source component having to know about any of it.
The Domain-Driven Design Parallel
Section titled “The Domain-Driven Design Parallel”The concept is borrowed, deliberately, from domain-driven design.
Eric Evans’s Domain-Driven Design: Tackling Complexity in the Heart of Software — the big blue book, published 2003 — introduced the term bounded context for a region of a software system where a specific vocabulary applies and specific invariants hold. Inside the Sales bounded context, a Customer is one thing — with sales-specific attributes, sales-specific relationships, sales-specific business rules. Inside the Support bounded context, a Customer is something else — different attributes, different relationships, different rules. The same word means different things in different contexts, and the architectural discipline is to make the contexts explicit so the meaning doesn’t get confused.
Bounded contexts have been one of the most enduring contributions of DDD. Microservices architectures, modular monoliths, federated GraphQL, and a long list of modern backend patterns lean on the bounded-context concept to organize complexity. The pattern is durable because it matches how humans actually think about complex systems — where am I, what’s the local vocabulary, what are the rules here.
Applications need this too. A frontend application isn’t a single uniform field of components. It’s a structured set of places — surfaces, features, entities, modes — each with its own vocabulary, its own conventions, its own meaning. The button in one place has different application-level meaning than the same button in a different place. The component model alone doesn’t capture this. Boundaries do.
A boundary, in the Kitsune sense, is a region of the DOM that supplies application-level context to events fired inside it. The context survives the boundary’s contents — it doesn’t matter which specific component fired the event, or how deeply nested the component was. The boundary supplies the context to anything inside it. The DOM’s containment relationships do the work.
The Surface/Feature/Entity Vocabulary
Section titled “The Surface/Feature/Entity Vocabulary”A working boundary protocol needs a small, consistent vocabulary. Kitsune’s choice — settled after a few iterations on real applications — is three dimensions: surface, feature, and entity.
Surface names where in the application this region appears. profile-editor. user-table. table-row. settings-page. checkout-cart. The surface is the answer to where am I. Surfaces nest — a user might be on the settings-page surface, inside a profile-editor surface, inside a display-name-field surface. The nesting captures the logical hierarchy of the UI.
Feature names which product capability the region belongs to. preferences. billing. admin. onboarding. checkout. The feature is the answer to which slice of the product. A single user might cross multiple features in a session — visit billing, configure preferences, edit a profile — and the feature attribution helps analytics, audit, and observability distinguish actions across these slices.
Entity names what the region is about. entity-type="user" plus entity-id="user_123". entity-type="invoice" plus entity-id="inv_4567". The entity is the answer to what’s being acted on. When the user clicks delete in a table row, the entity context says which row’s entity is being deleted. The button doesn’t need to know its own row; the row boundary tells it.
These three are enough to disambiguate most application actions. A profile.saved event with surface: 'profile-editor', feature: 'preferences', entity: { type: 'profile', id: 'user_123' } is a complete, unambiguous fact about what happened. The same event from a different boundary would have different context and be a different fact.
Other dimensions can be added when needed — route (which URL path the user is on), mode (admin vs. self, edit vs. view), tenant (in multi-tenant applications), experiment-arm (in applications running experiments). The vocabulary is extensible; the three core dimensions (surface, feature, entity) carry most of the architectural weight.
Boundaries Supply Context to Events
Section titled “Boundaries Supply Context to Events”The mechanism is the DOM context tree from Chapter 21, formalized.
A boundary is declared in markup with explicit attributes:
<kit-boundary surface="user-table" feature="admin"> <kit-boundary surface="table-row" entity-type="user" entity-id="user_123"> <kit-button meta-event="user.delete_requested">Delete</kit-button> </kit-boundary></kit-boundary>The button doesn’t carry the full context. It declares the event it represents (user.delete_requested) and lets the boundaries above it supply everything else.
When the button is activated, the click bubbles up through the DOM. A delegated listener installed by the outer <kit-boundary> observes the click, walks up from the target collecting context, and constructs an enriched event:
{ type: 'user.delete_requested', context: { surfaces: ['user-table', 'table-row'], // outer to inner surface: 'table-row', // most specific feature: 'admin', entity: { type: 'user', id: 'user_123' } }}The capability modules from Chapter 35 receive this enriched event. The audit module records the action with the full context. The analytics module sends the event to the analytics provider with surface, feature, and entity attached. The permission module decides whether the action is allowed based on the entity and the user’s role. Each module gets what it needs without the button or the boundaries having to know which modules exist.
The same button, rendered inside a different row, produces a different event:
{ type: 'user.delete_requested', context: { surfaces: ['user-table', 'table-row'], surface: 'table-row', feature: 'admin', entity: { type: 'user', id: 'user_456' } // different user }}The button is the same. The row boundary supplies the different entity. The audit log records the right user being deleted. The architecture didn’t have to pass the user ID through props down to the button; the row boundary owns it and the event inherits it.
Nested Boundaries and Inheritance
Section titled “Nested Boundaries and Inheritance”Boundaries compose through nesting. Inner boundaries supply more-specific context; outer boundaries supply less-specific context. The architecture follows the DOM’s structural hierarchy directly.
A realistic example:
<kit-boundary surface="app"> <kit-boundary surface="settings-page" feature="preferences"> <kit-boundary surface="profile-section" entity-type="profile" entity-id="me"> <form> <kit-boundary surface="display-name-field"> <kit-input meta-event="profile.field_changed" ...></kit-input> </kit-boundary> </form> </kit-boundary> </kit-boundary></kit-boundary>An event from the input inherits the full chain:
{ type: 'profile.field_changed', context: { surfaces: ['app', 'settings-page', 'profile-section', 'display-name-field'], surface: 'display-name-field', feature: 'preferences', entity: { type: 'profile', id: 'me' } }}The runtime walks from the event target up to the outermost boundary, collecting attributes at each level. Closer boundaries override more-distant ones. The inheritance is exactly the same shape as CSS custom property inheritance (Chapter 21) or accessibility-tree inheritance — containment in the tree means inheritance of context.
The inheritance is also subtractive when needed. A boundary can explicitly clear an inherited attribute by setting it to false or the empty string, signaling that the inner region doesn’t belong to the outer surface stack. This is rare but useful — a modal that pops up over the settings page might want to declare its own feature context rather than inheriting preferences, because the modal is about something different.
The nesting pattern is what makes the boundary architecture compose at application scale. Adding a new section is adding another boundary. Removing a section is removing a boundary. The events inside the section automatically pick up the right context. The architecture doesn’t need to be edited to add a new region — it follows the DOM’s structure naturally.
Programmatic Boundaries
Section titled “Programmatic Boundaries”Not every boundary is declared in markup. Some are established programmatically — particularly by framework adapters that need to bridge the runtime’s boundary model to whatever rendering system the application is using.
A router adapter for React Router, Next.js, or Astro can attach a boundary at the document root that supplies the current route’s context:
runtime.attachBoundary(document.documentElement, { context: { route: location.pathname, pathname: location.pathname, params: extractParams(location.pathname) }})When the route changes, the adapter updates the boundary’s context. Events fired anywhere in the document inherit the current route context.
Programmatic boundaries are also useful for boundary regions that don’t have a natural HTML element to host them. A user session boundary that supplies the current user’s identity context might be attached to <html> or <body> from JavaScript, rather than declared in markup, because the user identity isn’t a rendered region per se.
The mechanism is symmetric. A declarative boundary (<kit-boundary>) and a programmatic boundary (runtime.attachBoundary(element, ...)) produce the same context inheritance. The runtime treats them identically.
Custom Elements as Boundary Hosts
Section titled “Custom Elements as Boundary Hosts”Custom elements can themselves declare boundary context. A <user-profile> custom element representing a user profile might internally establish an entity boundary for that user:
class UserProfile extends LitElement { @property() userId = ''
connectedCallback() { super.connectedCallback() runtime.attachBoundary(this, { context: { surface: 'user-profile', entityType: 'user', entityId: this.userId } }) }
// ...}The component renders its content. The boundary established on the component supplies context to events inside. The component author thinks about the boundary as part of the component’s design — what context does this component supply to anything inside it? — and the runtime handles the inheritance.
This is a useful pattern because it lets reusable components carry their context with them. A <user-profile> placed in different parts of the application supplies the same kind of context wherever it’s used. The application doesn’t have to remember to wrap each instance in an explicit boundary; the component is the boundary.
How Modules Use Boundary Context
Section titled “How Modules Use Boundary Context”The capability modules from Chapter 35 use boundary context as their primary disambiguation tool.
The analytics module reads surface, feature, and entity from each event and sends them to the analytics provider as event properties. The provider can now slice user behavior by surface and feature — how many people clicked Save in profile-editor vs. password-form? — without the module’s code having to know which buttons came from which surfaces.
The audit module reads the entity context to record which entity was acted on. The audit log entry shows the affected user’s ID, the actor’s ID (from a separate user-identity provider), the action that was taken, and the surface where it originated. Reading the audit log later, an investigator can reconstruct what happened in what context.
The permission module reads the entity context to make access decisions. Can the current user perform user.delete_requested on user_123? The module looks at the entity and the actor’s role and decides. The button didn’t have to encode the permission check; the module does it centrally based on the event’s context.
The observability module uses the surface stack to construct a trace tag. Spans grouped by surface=profile-editor are easy to analyze. Errors in a specific feature are easy to filter. The observability dashboard becomes navigable by application geography.
The pattern is consistent. Each module looks at the parts of the boundary context it cares about and ignores the rest. The button supplied only the action name. The boundaries supplied the disambiguating context. The modules consumed the enriched event. Nothing was repeated. Nothing was passed through props.
Boundaries Are Not Components
Section titled “Boundaries Are Not Components”The chapter has to land one architectural distinction directly. A boundary may be implemented as a component, but it isn’t merely UI. It’s an application context zone.
A boundary is a semantic artifact. It exists to declare a piece of the application’s structure. It usually has no visual presence — <kit-boundary> renders as a display: contents element that doesn’t affect layout. Its purpose is architectural, not visual.
This is why boundaries live with the runtime, not with the component library. The boundary protocol is part of the missing-layer architecture. The component library (Part V) uses boundaries when appropriate, but the boundary concept is more fundamental than any particular component. Server-rendered HTML can use boundaries. AI-generated markup can use boundaries. A React application running inside a Kitsune runtime can use boundaries. The boundary is a thin wrapper element with attributes; the meaning comes from the surrounding architecture.
A <kit-button> is a component. A <kit-boundary> is a boundary. They participate in the same architecture, but they do different things. Components describe interface and intent. Boundaries describe application geography.
Bridge to Events and Commands
Section titled “Bridge to Events and Commands”The chapter has established what boundaries are and how they supply context. The next chapter takes the events and commands themselves seriously — what events are (facts about what happened), what commands are (requests for things to happen), and how the runtime distinguishes between them.
The combination — components, boundaries, events, commands, capability modules — is the architecture. Each piece carries one architectural responsibility. The composition gives the missing layer its shape.
Exercise: Model an Admin Dashboard
Section titled “Exercise: Model an Admin Dashboard”Sketch the DOM structure for an admin dashboard with these regions:
- The admin application root.
- A Users route showing a table.
- The table itself.
- A row in the table for
user_123. - An action menu on the row.
- A confirmation dialog that appears when the user clicks Delete.
Add boundary metadata at each level:
<kit-boundary surface="admin-app" feature="admin"> <kit-boundary surface="users-route" route="/admin/users"> <kit-boundary surface="user-table"> <kit-boundary surface="user-row" entity-type="user" entity-id="user_123"> <kit-boundary surface="row-actions"> <kit-button meta-event="user.delete_requested">Delete</kit-button> </kit-boundary> </kit-boundary> </kit-boundary> </kit-boundary></kit-boundary>Construct the event that would fire from the Delete button. What’s the surface stack? What’s the feature? What’s the entity?
Now imagine the user clicks Delete on a different row, user_456. What changes in the event’s context? What stays the same?
Then think about the confirmation dialog. When the user confirms, the application fires user.delete_confirmed. What boundary should that event come from? Should the dialog establish its own boundary, or inherit from the row? What context would each option produce?
Reflect on:
- Which context was inherited from outer boundaries?
- Which context was specific to the inner-most boundary?
- Which modules might care about each piece of context?
- How would this reduce repeated props or imports across the table’s components?
- If you added a second
users-routefor a different tenant, what would change?
The point is to feel that boundaries are cheap — adding one is one line of markup — and that they replace a substantial amount of prop-drilling and context-passing the component-tree-only approach would require. The application’s geography becomes legible in the markup itself.