cancel
Showing results for 
Search instead for 
Did you mean: 

Best Practices for API Design

iminiailo
Adobe Team

API design is a crucial part of software development. APIs directly influence system maintainability and extensibility depending on extraction and extension points introduced to the system and how they are organized in the codebase.

 

Due to the extensive size of Magento 2, we introduced a layer of Service Contracts (public APIs) in the scope of each Bounded Context (in terms of Domain Driven Design). These public APIs explicitly expose the services provided by each specific business domain and serve as an entry point to the domain, playing a role of Facades to mask complex implementation logic behind the scenes. In Magento 2, public APIs should be placed in separate API modules. All other modules depend on the API module, while implementations may be easily swapped via di.xml configuration.

 

For the scope of this article, we use the Multi-Source Inventory (MSI) project.  For example, if a module is named Inventory, its APIs are declared in a module named InventoryApi.

msgiblog1.png

 

Guidelines

Service interfaces, which should be exposed as Web APIs (REST/SOAP/GraphQL), are placed under the Api namespace. While all other APIs, including explicit extension points like Chain or Composite implementations, are placed under Model namespace.

 

Under the Api\Data namespace Magento stores Data Transfer Object (DTO) interfaces which represent entities operated by services declared on the level above.

 

All the public APIs are marked with PHP annotation @api. Magento keeps Semantic Versioning for modules following the assumption that modules do not have to access private implementation of other modules, all the integration supposed to be implemented via public APIs only. It is important for third-party developers to have dependencies just on the code marked with @api annotation to make their Magento installation being easily upgradeable. As along the product releases Magento follows Backward Compatibility policies for APIs only and thus guarantees that Public APIs would not be affected. Along with that, reserves the right to change all non-@api-marked instances which are considered as private module’s implementation. In most cases, modifications of non-api code will only trigger PATCH module version bumps. Whereas changes in public code always trigger MINOR or MAJOR version bumps.

 

Such differentiation should simplify life for both merchants and extension developers. Both parties want things to be as cost-effective as possible. For merchants, this means that upgrades should be as low-touch as possible, while developers want their extensions to be forward-compatible for as long as possible. By specifying dependencies on modules’ public contract only, merchants can be sure that they will not encounter backwards incompatibilities with subsequent patch versions, and that their upgrade path will remain healthy.

 

A PHP Interface in Magento can be used several ways by the core product and extension developers.

 

  • As an API: is a set of interfaces that a module provides to other modules and they supposed to be called and used to achieve the goal.
  • As a Service Provider Interface (SPI):  is a set of interfaces that a module uses internally and allows their extension and implementation by other modules.
  • As both: APIs and SPIs are not mutually exclusive.

 

After the code is released, both API and SPI can be evolved in a backwards-compatible way. But both have their specific limitations.

 

Limitations

 

After the API is released, new behavior can be added, but none can be removed. Existing behavior cannot be modified.

 

Examples of APIs:

 

msgiblog2.png

msgiblog3.png

 

After SPI is released, existing behavior can be removed, but none can be added. Existing behavior cannot be modified.

 

Examples of SPIs:

 

msgiblog4.png

msgiblog5.png

msgiblog6.png

 

To prevent over-complicating the development for third-party developers, Magento does not differentiate between APIs and SPIs. As it is still possible that one Magento customization will use a particular interface in an API way, another may provide an implementation for an existing interface (SPI usage). As a result, SPIs are annotated the same way as APIs. Third party developers are responsible to decide how they use interfaces declared in external modules and, depending on those declarations, specify dependency in the module’s composer.json file.

 

  • MAJOR module version dependency should be specified if the developer uses/calls external interfaces in the module (API).
  • MAJOR+MINOR version should be specified if module implements interface declared elsewhere.

 

Example composer.json:

msgiblog7.png

 

Taking into account the strict Backward Compatibility policy which Magento has to follow, and that Magento does not distinguish between SPIs and APIs, we have the following prohibited code changes for code marked with @api:

 

  • Interface/class removal
  • Public and protected method removal
  • Introduction of a method to a class or interface
  • Static function removal
  • Adding parameters in public methods
  • Adding parameters in protected methods
  • Method argument type modification
  • Modification of types of thrown exceptions (unless a new exception is a sub­type of the old one)
  • Constructor modification
  • Modifying the default values of optional arguments in public and protected methods 
  • Removing or renaming constants

 

See DevDocs Backward compatible development to learn more about prohibited code changes for public code.

 

There is no evolution for @api interfaces in Backward Compatible way. Any change brought to the interface, regardless of adding or removal behavior, will lead to breaking some clients. The best action to circumvent these circumstances is to make interfaces as atomic as possible. Doing so, we come to the idea of Functional objects (Functors), an interface which consists of the only method.

 

In this case, if the interface should be modified or removed, it is just marked as @deprecated and a new version of the interface is introduced, not affecting other services and methods. The more granularly a service is designed, the less impact modifications in this interface would introduce to other services. That’s why good object-oriented programming in the service layer is basically functional programming:

 

  • Constructor injection, when all external dependencies are constructed first and then passed as constructor arguments to the object being instantiated.
    Constructor injection is preferable over Setter and Interface DI injection types as it can be used to ensure the client object is always in a valid state, as opposed to having some of its dependencies reference null (not set). This can be a first step towards making objects immutable.
  • Immutable state, when the internal state cannot be modified after object is created
  • Single responsibility principle 
    “A class should have only one reason to change” Robert C. Martin
  • Uniform interfaces
    Fundamental to the design of any REST service. As it simplifies and decouples the architecture, which enables each part to evolve independently.
  • Data transfer objects (DTO) passed across services
    DTOs don’t have any behavior and represent containers of data transferable via wire. Do not contain any business logic that would require testing. Usage of DTOs in services communication is a first step towards Microservice architecture which supports decoupling of technology stack between different Bounded Contexts.

 

Bringing these concepts to an extreme leads to single-method, immutable services which manipulate with DTOs. 

msgiblog8.png

msgiblog9.png

 

Having single-method services, it is very important to provide proper names to them. As in desirable state, developers should be about to quickly glance at service name to know what this service is responsible for, without opening the listing of the service in an IDE. It’s even better when the responsibility of such self-explanatory interfaces is clear not only to developers but also other stakeholders involved into the project. This Ubiquitous Language in Domain Driven Design.

 

Sometimes when we are not sure what is a proper name for the entity or service, we ask for feedback from the Community.

msgiblog10.png

 

Such naming may seem a bit unusual, as there are many recommendations to use nouns naming classes which represent type of objects. This rule is applicable for classes which represent "things/entities". Developers want to name them with nouns, and methods inside these classes represent actions over the entity so that methods usually represented by verbs.

 

But this approach does not work with Functional Objects that represent "action", so naming them with a verb is more appropriate.

 

It was discussed to make a more radical shift towards Functional Objects using magic method __invoke.

msgiblog11.png

msgiblog12.png

 

The following are Pros and Cons in the approach with __invoke.

 

Pros:

  • The main reason for __invoke usage proposal was elimination of unneeded execute methods, which look a bit artificial and redundant:
    msgiblog13.png

Cons:

  • No autocomplete in PHP Storm IDE accessing via $this:
    msgiblog14.png
  • Unit tests look frustrating and non-intuitive:
    msgiblog15.png
  • We will mix approaches, as Magento still has other Service Contracts (Repositories) that cannot be used as Function Objects as they contain more than one method inside.

 

After completing a short survey with the Community, we decided to use execute method.

msgiblog16.png

https://twitter.com/iminyaylo/status/938122403443552256

 

The rare exception from the rule of having single-method interfaces are Magento Repositories. Repository interfaces are usually provided to manage domain entities and methods listed inside these interfaces should follow next semantic:

 

msgiblog17.png

 

But that is not a strict rule, and the list of the methods could be shorter if based on the business requirements particular entity doesn't have some of the operation(-s). It is important to notice here that list of the methods in the Repository interface could NOT be wider than methods mentioned above, as it is not recommended to add other methods with their own semantic to the Repository interface (those methods are recommended to be put into some dedicated Services).

 

In Magento 2, Repositories are considered as an implementation of Facade pattern which provides a simplified interface to a larger body of code responsible for Domain Entity management. The main intention is to make API more readable and reduce dependencies of business logic code on the inner workings of a module, since most code uses the facade, thus allowing more flexibility in developing the system.

 

But internally, Repository still proxies calls to dedicated command-services.

msgiblog18.png

 

Each Service should follow the CQRS semantic and represent Command or Query, but not both.

  • Queries: Return a result and do not change the observable state of the system (are free of side effects).
  • Commands: Change the state of a system but do not return a value (return void).

 

More Information and Documentation

More examples of designing Service Contracts for Magento 2 could be found in the scope of Multi-Source Inventory (MSI) project. The project wiki provides information and discussions of the most interesting architectural changes implementing these best practices and Service Contracts.

 

We recommended reviewing the Magento Service Isolation design document which describes the Modularity approach being introduced to Magento codebase which would help to split the Monolith on independent and isolated set of Services. See DevDocs Service Contracts in the Technical Guidelines for development guidelines and requirement