From API Design Patterns by JJ Geewax

This article covers:

  • How to manage many-to-many relationships implicitly, without an additional association resource.
  • The benefits and drawbacks of implicit rather than explicit association
  • How to use custom methods to associate resources together
  • Handling issues with data integrity of associated resources

Take 40% off API Design Patterns by entering code fccgeewax2 into the discount code box at checkout at

In this article, we’ll explore an alternative pattern for modeling many-to-many relationships that relies on custom Add and Remove methods to associate (and disassociate) two resources. This pattern allows consumers to manage many-to-many relationships without introducing a third association resource as a necessary requirement.


Sometimes we have a need to keep track of the relationships between resources, and sometimes those relationships can be complicated. In particular, we often have to handle situations where two resources can both “have many” of one another, known as a many-to-many relationship.

Although the association resource pattern offers a flexible and useful way to model this type of relationship, it seems worth asking whether there could be a simpler way to do this if we can live with some limitations. Given some restrictions, can we make the API a bit simpler and more intuitive? And if true, what are the specific limitations in order to use this simplification? This pattern explores a simpler alternative to the association resource pattern.


As you might guess, there certainly are simpler ways of representing and manipulating many-to-many relationships in an API, but they come with several restrictions. In this particular pattern, we’ll look at a way of hiding the individual resources that represent these relationships and use custom methods to create and delete associations. Let’s start first by summarizing the methods and then we’ll explore the specific limitations that come up when relying on this design pattern.

At the most basic level, we simplify the API by completely hiding the association resource from consumers and instead manage the relationship using “Add” and “Remove” custom methods. These methods act as shortcuts to create and delete associations between the two resources in question and hide all of the details about that relationship except for the fact that it exists (or doesn’t exist). In the classic example of users that can be members of multiple groups (and groups that obviously contain multiple users), this means that we could use these Add and Remove methods to represent users joining (Add) or leaving (Remove) a given group.

In order to adopt this pattern, let’s look at a few of the limitations mentioned earlier that we’ll need to take into consideration. First, because we only ever store the simple fact that two resources are associated, we won’t be able to store any metadata about the relationship itself. This means, for example, that if we use this pattern for managing users as members of groups, we can’t store details about the membership such as the date a user joined a group or any specific role a user might play in a given group.

Next, because we’re going to use custom methods to add and remove the association between the resources, we must consider one of the resources to be the “managing resource” — sort of like one being the parent of the other. More concretely, we’ll have to choose whether we “add users to groups” or “add groups to users”. If the former, then the user resources are the ones being passed around and group resources are “managing” the relationship. If the latter, then users are managing the relationship as groups are passed around. Thinking in terms of code (in this case, in object-oriented programming style), the managing resource is the one which has the “add” and “remove” methods attached to it, as shown in Listing 1.

Listing 1. Code snippets for the two alternatives when choosing a managing resource.

 group.addUser(user);  #A
 user.addGroup(group);  #B

#A When a group manages the relationship, we add users to a given group.

#B When a user manages the relationship, we add groups to a given user.

Sometimes the choice of a managing resource is obvious, but other times it might be more subtle where both options make sense. In some cases, neither one seems intuitive as the managing resource, but at the end of the day using this pattern requires that we choose a single managing resource. Assuming we can live with both of these limitations, let’s look at the specifics of how this particular pattern must work.


Once we’ve identified the “managing resource”, we can define Add and Remove custom methods. The full name of the method should follow the form of Add<Managing-Resource><Associated-Resource> (and likewise for Remove). For example, if we’ve users who can be added and removed from groups, the user is the associated resource and the group managing the resource. This means that the method names are AddGroupUser and RemoveGroupUser.

These methods should accept a request that contains both a parent resource (in this case, the managing resource) and the identifier of the resource being added or removed. Notice that we use the identifier only and not the full resource. This is because the other information is extraneous and potentially misleading if consumers were led to believe that they had the ability to associate two resources and update one of them at the same time. A summary of the Add and Remove methods and their HTTP equivalents is shown below in Table 1.

Table 1.  Add and Remove method summary



HTTP equivalent

Add user 1 to group 1


  parent: "groups/1",

  user: "users/1"


POST /groups/1/users:add


{ user: "users/1" }

Remove user 1 from group 1


  parent: "groups/1",

  user: "users/1"


POST /groups/1/users:remove


{ user: "users/1" }

Listing associated resources

To list the various associations, we rely on customized List standard methods that look like the alias methods in the association resource method. These methods list the various associated resources, providing pagination and filtering over the results. Because we’ve two ways to look at the relationship (for example, we might want to see which users are members of a given group as well as which groups a given user is a member of) we’ll need two different methods for each of the scenarios.

These methods follow a similar naming convention to the Add and Remove methods, with both resources in the name of the method. Using our users and groups example, we provide two different methods to list the various users and groups given a specific condition: ListGroupUsers provides the list of users belonging to a given group and ListUserGroups provides the list of groups that a given user is a member of. As with other custom methods, these follow a similar HTTP mapping naming convention but rely on an implicit sub-collection, summarized below in Table 2.

Table 2.  List method summary



HTTP equivalent

What users are in group 1?


  parent: "groups/1"


GET /groups/1/users

What groups are user 1 a member of?


  parent: "users/1"


GET /users/1/groups

Data integrity

One common question arises when we run into issues of duplicate data. For example, what if we try to add the same user to a group twice? On the other hand, what if we attempt to remove a user from a group that they aren’t currently a member of?

The behavior in these cases is going to be similar to deleting a resource that doesn’t exist and creating a duplicate (and therefore conflicting) resource. This means that if we attempt to add a user twice to the same group, our API should respond with a conflict error (e.g. 409 Conflict) and if we attempt to remove a user from a group that doesn’t exist, we should return an error expressing a failed assumption (e.g. 412 Precondition Failed) to signal that we’re not able to execute the requested operation.

This means that if a consumer only cares about ensuring that a user is a member of a given group, they can treat these error conditions as their work complete. If we want to be sure that Jimmy is a member of group 2, a successful result or a conflict error are both valid results as both signify that Jimmy is currently a member of the group as we intended. The difference in response code conveys to us whether we were the ones responsible for the addition to the group, but either one conveys that the user is now a member of the group.

Final API definition

A full example of this pattern implemented using the same users and groups example is shown in Listing 2. As you can see, this is much shorter and simpler than relying on an association resource, but we lack the ability to store metadata about the relationship of users belonging to groups.

Listing 2. Final API definition using the Add/Remove pattern.

 abstract class GroupApi {
   static version = "v1";
   static title = "Group service";
   GetUser(req: GetUserRequest): User;
   // ... #A
   GetGroup(req: GetGroupRequest): Group;
   // ... #A
   AddGroupUser(req: AddGroupUserRequest): void;  // #B
   RemoveGroupUser(req: RemoveGroupUserRequest): void;  // #B
   ListGroupUsers(req: ListGroupUsersRequest): ListGroupUsersResponse;  // #C
   ListUserGroups(req: ListUserGroupsRequest): ListUserGroupsResponse;  // #D
 interface Group {
   id: string;
   userCount: number;
   // #E
   // Note we do not in-line the users here since the list could still be very
   // long. To see the list of users, use ListGroupUsers().
 interface User {
   id: string;
   emailAddress: string;
 interface ListUserGroupsRequest {
   parent: string;
   maxPageSize?: string;
   pageToken?: string;
   filter?: string;
 interface ListUserGroupsResponse {
   groups: Group[];
   nextPageToken: string;
 interface ListGroupUsersRequest {
   parent: string;
   maxPageSize?: number;
   pageToken?: string;
   filter?: string
 interface ListGroupUsersResponse {
   users: User[];
   nextPageToken: string;
 interface AddGroupUserRequest {
   parent: string;
   user: string;
 interface RemoveGroupUserRequest {
   parent: string;
   user: string;

#A For brevity we’re going to omit all of the other standard methods for the User and Group resources.

#B We use AddGroupUser and RemoveGroupUser to manipulate which users are associated with which groups.

#C To see which users are in a given group, we use an implicit subcollection mapping to a ListGroupUsers method.

#D To see which groups a user is a member of, we use the same idea and create a ListUserGroups method.

#E Note we don’t in-line the users here because the list could be long. To see the list of users we instead use ListGroupUsers.


As we noted at the start of this article, the primary goal of this pattern is to provide the ability to manage many-to-many relationships without taking on the complexity of a full-blown association resource. In exchange for this simplification, we make a few trade-offs in the form of feature limitations.

Non-reciprocal relationship

Unlike an association resource, using Add and Remove custom methods requires that we choose one of the resources to be the “managing resource” and another to be the managed resource. We need to decide which resource is the one being added (and removed) to (and from) the other. In many cases this non-reciprocity is convenient and obvious, but other times this can seem unintuitive.

Relationship metadata

By using custom Add and Remove methods rather than an association resource, we give up the ability to store metadata about the relationship itself. This means that we won’t be able to store any information besides the existence (or lack of existence) of a relationship. We can’t keep track of when the relationship was created or anything specific to the relationship and instead need to put that elsewhere.


  1. When would you opt to use custom Add and Remove methods rather than an association resource to model a many-to-many relationship between two resources?
  2. When associating Recipe resources with Ingredient resources, which one is the managing resource and which would be the associated resource?
  3. What would the method be called to list the Ingredient resources that make up a specific Recipe?
  4. When a duplicate resource is added using the custom Add method, what should the result be?


  • For scenarios where association resources are too heavyweight (e.g. there’s no need for any extra relationship-oriented metadata), using Add and Remove custom methods can be a simpler way to manage many-to-many relationships.
  • Add and Remove custom methods allow an associated resource, i.e. the subordinate, to be added or removed in some association to a managing resource.
  • Listing associated resources can be performed using standard List methods on the meta-resources.

If you want to learn more about the book, check it out on Manning’s liveBook platform here.