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 elementscuid— mixin that adds an auto-generated UUID primary keymanaged— mixin that addscreatedAt,createdBy,modifiedAt,modifiedByAssociation/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 functionThe 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:
| Phase | Purpose | Example |
|---|---|---|
before | Validate / guard / set defaults | Reject blank names, normalize input |
on | Implement custom business logic | Custom action handlers |
after | Enrich results / emit side effects | Compute 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:
| Annotation | Scope | Controls |
|---|---|---|
@requires | Service, entity, action | Which roles may access at all |
@restrict | Entity | Fine-grained grant/to/where per event |
The showcase defines three conceptual roles:
Viewer— read-only access to all dataEditor— 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.skipfor 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:
| Protocol | Path | Use 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 eventThe domain event People.Changed.v1 is declared in the service CDS and appears in the AsyncAPI spec generated by npm run asyncapi.