Skip to content

CAP Architecture Overview

This document explains how the layers in this sample fit together.

Request-to-Response Flow

Layer Responsibilities

Domain Model (db/schema.cds)

The domain model defines what data looks like, independent of how it is served.

Key CDS concepts demonstrated:

  • entity — a persisted table with typed elements
  • cuid — mixin that adds an auto-generated UUID primary key
  • managed — mixin that adds createdAt, createdBy, modifiedAt, modifiedBy
  • Association / Composition — relationships between entities
  • @assert.range / @assert.format — built-in server-side validations
  • @mandatory — not-null + presence check
  • @PersonalData — GDPR annotation for data privacy auditing
  • @cds.persistence.journal — opt-in schema change journal (migration safety)
db/schema.cds
  └── star.wars namespace
        ├── Film          (draft-enabled, episode_id enum)
        ├── People        (PersonalData annotations)
        ├── Planet
        ├── Species
        ├── Starship
        ├── Vehicle
        ├── Show          (show_type enum, draft-enabled)
        │   └── Episode   (composition child — cascade delete)
        ├── Show2People   (physical M:N junction — Show ↔ People)
        ├── Film2People, Film2Planets, Film2Starships,
        │   Film2Vehicles, Film2Species  (M:N junction tables)
        ├── Episode2People, Episode2Planets, Episode2Starships,
        │   Episode2Vehicles, Episode2Species  (M:N junction tables)
        ├── Show2Planets, Show2Starships, Show2Vehicles, Show2Species
        │   (CDS define view over Episode2* — not physical tables)
        ├── Media          (define view — UNION of Film + Show)
        ├── MediaCharacters, MediaPlanets, MediaStarships,
        │   MediaVehicles, MediaSpecies  (aggregation views)
        └── CloneWarsChronologicalOrder
            (view — 133 episodes with canonical chronological sequence)

define view in CDS creates a SQL view rather than a physical table. It is the right choice when the data can be fully derived from another entity — there is no value in storing it redundantly. Show2Planets (and its siblings) are a concrete example: Wookieepedia show pages list no per-show relationships, but each episode page lists the planets it features. By defining the show-level view as an aggregation over Episode2Planets, show-level data is always correct and requires no separate load step.

Service Layer (srv/*-service.cds)

Services project domain entities into API-facing views. The key insight:

  • You can expose different shapes for different consumers
  • You can make things read-only that are writable in the model
  • You can add custom actions and functions beyond CRUD
srv/people-service.cds (StarWarsPeople service)
  ├── People          ← writable projection, @odata.draft.enabled
  ├── Film            ← @readonly projection
  ├── Planet          ← @readonly projection
  ├── ...
  ├── action rename() ← bound action on People
  └── function countByGender() ← unbound function

The StarWarsEpisode service (srv/episode-service.cds) exposes Episodes and Episode2* junctions as read-only projections across all three protocols. It has no .js handler because there is no write path — all episode data arrives through the Show draft workflow or via data loading.

See Shows & Episodes for a full explanation of the domain model.

Handler Lifecycle (srv/*.js)

Every mutable request passes through three phases in order:

PhasePurposeExample
beforeValidate / guard / set defaultsReject blank names, normalize input
onImplement custom business logicCustom action handlers
afterEnrich results / emit side effectsCompute displayTitle, fire events

If no on handler is registered, CAP's generic provider handles CRUD automatically. If an on handler IS registered, it fully replaces the generic handler — you own the response.

See people-service.js for all three phases in one file.

Authorization (srv/services-auth.cds)

CAP authorization is annotation-driven. Two annotations work together:

AnnotationScopeControls
@requiresService, entity, actionWhich roles may access at all
@restrictEntityFine-grained grant/to/where per event

The showcase defines three conceptual roles:

  • Viewer — read-only access to all data
  • Editor — can create/update People (the only writable entity in the showcase)
  • Admin — full access including delete and admin actions

See services-auth.cds for the full matrix.

Profile Extensions (db/hana/, db/sqlite/, db/postgres/)

CAP loads profile-specific CDS files on top of the base schema. This is used for:

  • DB-specific SQL functions or native types
  • Overriding persistence behavior (e.g., @cds.persistence.skip for views on one DB)
  • Adding calculated fields or indexes that only make sense on one backend

See /architecture/profiles for what each profile changes.

Protocols

This service exposes three protocols simultaneously from the same model:

ProtocolPathUse case
OData v4/odata/v4/<Service>/Fiori UI, standard SAP integration
REST/rest/<Service>/Simple HTTP clients, microservices
GraphQL/graphql/Flexible querying, developer tooling

The @protocol: ['odata-v4', 'graphql', 'rest'] annotation on each service enables all three.

The four Fiori web applications (People, Media Browser, Film Editor, Show/Episode Editor) consume these protocols via OData v4. See Fiori Apps for a walkthrough of each application.

Event Flow

When a People record is created or updated, three things happen:

1. POST /odata/v4/StarWarsPeople/People


2. before CREATE  ─→ validate name is non-empty


3. on CREATE  ─→ (generic CRUD — no custom on-handler for this event)


4. after CREATE
        ├─→ alert.notify(...)     ← SAP Alert Notification
        └─→ this.emit('People.Changed.v1', data)  ← AsyncAPI domain event

The domain event People.Changed.v1 is declared in the service CDS and appears in the AsyncAPI spec generated by npm run asyncapi.