Motivation#
Many-to-many relationships (e.g., Users ↔ Groups) are common and sometimes carry metadata about the relationship itself (join time, role, etc.). Exposing the join-table rows as first-class association resources gives consumers the ability to address an individual relationship and store metadata about it.
Overview#
- The canonical storage for many-to-many relationships is a join table.
- Expose that join table as a resource (e.g.,
Membership
orUserGroup
) so the API surface mirrors the schema:User
,Group
,Membership
. - Association resources are created/deleted like any other resource (create = join, delete = leave).
- Listing and filtering the association collection reveals relationships; retrieving a single association returns metadata about that relationship.
Implementation (key decisions)#
Naming#
- Choose a meaningful name in context:
CourseEnrollment
,CourseMembership
,Membership
,Association
, etc.
Standard methods#
- Implement standard resource methods:
Create
,Get
,Update
,Delete
,List
. - If the association holds metadata, support
Get
andUpdate
.List
is needed for browsing.
Uniqueness#
- Enforce uniqueness: there should be only one association resource for the same pair of resources (e.g., one membership per
(userId, groupId)
). - Attempts to create a duplicate association should fail (e.g.,
409 Conflict
).
Read-only fields#
- The pointer fields that identify the two resources (e.g.,
userId
,groupId
) should be treated as output-only for updates. - To change an association’s endpoints, delete the old association and create a new one. Update requests that attempt to change the cross-reference fields should ignore them (or error depending on policy).
Association alias methods#
Provide convenience alias methods for common filtered queries, e.g.:
ListUserGroups({ userId })
— groups for a user (alias forListMemberships?filter=userId:...
)ListGroupUsers({ groupId })
— users in a group (alias forListMemberships?filter=groupId:...
)
Naming convention:
<OwningResource><ListedPlural>
—UserGroups
= groups owned by a user;GroupUsers
= users of a group.
Referential integrity#
Common DB behaviors:
Cascade
,Restrict
,Set null
,Do nothing
.For association resources, prefer either:
Restrict
— prevent deleting a resource while associations exist (return412 Precondition Failed
), orDo nothing
— allow deletion and leave dangling associations for the consumer to handle.
Avoid automatic cascade or bulk nulling due to risk of massive writes or delete avalanches.
Final API (example)#
- Top-level resources:
User
,Group
,Membership
. - Example endpoints/methods:
POST /memberships -> CreateMembership
GET /memberships/{id} -> GetMembership
PATCH /memberships/{id} -> UpdateMembership
DELETE /memberships/{id} -> DeleteMembership
GET /memberships -> ListMemberships
GET /groups/{groupId}/users -> ListGroupUsers (alias)
GET /users/{userId}/groups -> ListUserGroups (alias)
Membership
model storesuserId
,groupId
, plus metadata fields likerole
,expireTime
, etc.
Trade-offs#
- Flexibility: association resources are the most flexible representation (store relationship metadata, separate lifecycle).
- Complexity: adds API surface (extra resource + methods) and cognitive overhead (create membership vs.
JoinGroup
). - Separation of concerns: group description and membership list are retrieved separately (may feel split but is practical for large lists).
Exercises (brief)#
- Design an API associating users with chat rooms where
role
andjoin_date
are stored on theMembership
. - To retain join/leave history while preventing concurrent duplicate presences, give each
Membership
a unique id and anisActive
/leaveDate
field; on join, ensure no active membership for(userId, roomId)
exists.
Takeaway bullets#
- Association resources model many-to-many relationships and relationship-specific metadata.
- They require special handling: uniqueness constraints, read-only cross-reference fields, and considered referential-integrity policies.
- Alias listing methods improve ergonomics for common queries (
/users/{id}/groups
,/groups/{id}/users
).