Skip to main content

Design API for Multiple Different Clients

Today I want to talk about common design challenges related to architecture of robust APIs, designed to be consumed by multiple clients with different needs.

Our use case is the following: We need to build a N-Tier Web REST/SOAP API that is supposed to read/write data from a DB, perform some processing on that data and expose those methods to our API consumers.

In addition we have multiple different API clients each with different needs, meaning we can't just expose a rigid set of functions with a defined group of DTOs (Data Transfer Objects).

DTO vs POCO

Before start diving I want to explain shortly the difference between these two controversial concepts.

DTO

Objects that are designed to transfer data between edges (i.e. between processes, functions, server & clients etc'). Typically DTOs will contain only simple properties with no behavior.

POCO

Objects that are designed to reflect the internal business data model. For example if you have an eCommerce platform you will probably have the following POCOs:
  • Customer
  • Order
  • Store
  • Account

POCOs usually reflects in a way or another the DB tables schema. Depending on the specific BL architecture each POCO may contain functions that manipulates its related data.

Adapters

In order to accomplish our mission we will probably create a Web Service Layer (used like a door to the outside world, that encapsulate REST/SOAP configuration), Business Logic Layer and a Data Access Layer.

Below is a simplistic diagram of our tiers.



A common mistake is to expose internal POCOs directly to our clients:

  • Any internal POCO's structural change will be reflected to our clients
  • Any esoteric client request will force us to add/change our internal BLL methods and add/change our internal POCOs
  • Very difficult to serve multiple clients with different needs with the same set of POCOs

My suggestion is to create a new set of Adapters, one for each specific client, these Adapters 'sits' between our clients and our BLL.



The Adapter layer has two responsibilities:

  • Translate internal POCOs to client specific DTOs.
  • Expose different suited set of methods for each client.

All adapters communicate withe the BLL that contains common (cross Adapters) logic.

public CarCompanyCustomer  GetCustomer(string customerID)
{

   //* Adapter manipulates client's prodID
   int ID =   ConvertTo.customerID.Substring(0,2);

   //* internal POCO
   EntCustomer customer = BLL.GetCustomerSomeComplexInternalFunctionName(ID);
  
  //* External DTO
  CarCompanyCustomer DTOCustomer = newCarCompanyCustomer ()
  {
     ProductID = customer.ID,
     ModelName = customer.Name,
     ArrivalDate = customer.Birthdate,
     PhoneNumber = customer.ContactInfo.PhoneNum,
     Orders = customer.OrderList
  };

  return DTOCustomer;
}


Now each client have a suited set of methods and DTOs, changes in one client will not effect others.

POCO Design

In order to support such flexibility we need to carefully design our internal BLL's POCOs, I will demonstrate the challenge with an example.

Let's take the previous Customer POCO sample, we might had designed it to 'hold' a lot of data:

  • Name
  • ID
  • Birth Date
  • List of Orders (Item ID, Price, Shipping Status)
  • Address information (Home Address, Shipping Address)
  • Contact information (Email, Home Telephone Number, Work Telephone Number)

The problem is that this design suits scenarios were we often need to retrieve all Customer's data as a single unit.
But most probably not all our Web Service's clients will need the entire Customer data, we need to provide a better granular way to read small pieces of Customer's information. Designing too big entities will lead to unnecessary data retrieval from the DB.

The solution is to find the lowest common denominator, and to design very small entities with the minimum amount of fields, this way we will end-up with a lot of small objects that can suits all clients needs. Instead of a single giant Customer POCO we will design several dedicated objects:

EntCustomer:
  • Name
  • ID
  • BirthDate

EntOrder:
  • ItemID
  • Price
  • ShippingStatus

EntAddress:
  • HomeAddress
  • ShippingAddress

EntContactInfo:
  • Email
  • Home Telephone Number
  • Work Telephone Number

It requires more work but as compensate we end-up with a robust architecture. Web Service clients that need only partial Customer's data can do it now without causing unnecessary DB data retrieval.

public List<CarCompanyCustomer> GetCustomerList()
{
   //* internal POCO
   EntCustomer customers = BLL.GetCustomerListSomeComplexInternalFunctionName();
   List<CarCompanyCustomer> carCompanyCustomerList = List<CarCompanyCustomer>();
   foreach(customer in customers){

   //* External DTO
   CarCompanyCustomer DTOCustomer = newCarCompanyCustomer ()
   {
       ProductID = customer.ID,
       ModelName = customer.Name,
       ArrivalDate = customer.Birthdate,
       PhoneNumber = BLL.GetContactInfo(ID).PhoneNum,//* DB round-trips for each iteration
       Orders = BLL.GetOrders(ID) //* DB round-trip //*  DB round-trips for each iteration
   };

     carCompanyCustomerList.Add(DTOCustomer);
   }

  return carCompanyCustomerList;
}


The obvious question that pops-out now is how to handle opposite scenarios (like in the example above) where we have Web Service clients that need a all Customer's data.
With the given design we force our Adapters to make multiple BLL & DB calls in order to collect all the necessary Customer's pieces of information.

It seems that we improved the design for certain clients but worsen for others.

In order to overcome this obstacle we must improve our implementation to support also bulks retrieval of data instead of multiple DB iterations.
public List<CarCompanyCustomer> GetCustomersList()
{
     //* internal POCO
   List<EntCustomer> customers = BLL.GetCustomersListSomeComplexInternalFunctionName();

   //* extract Customers IDs
   List<int> IDs = customers.Select(cust => cust.ID);

   //* Retreive data in bulks instead of multiple DB iterations 
   List<EntContactInfo> contactInfoList = BLL.GetContactInfoBulk(IDs); 

   //* Retreive data in bulks instead of multiple DB iterations
   List<EntOrder> ordersList = BLL.GetOrdersBulk(IDs);

   List<CarCompanyCustomer> carCompanyCustomerList = List<CarCompanyCustomer>();
   foreach(customer in customers)
   {
       //* External DTO
       CarCompanyCustomer DTOCustomer = new CarCompanyCustomer ()
       {
          ProductID = customer.ID,
          ModelName = customer.Name,
          ArrivalDate = customer.Birthdate,

          PhoneNumber = contactInfoList.Where(contact.CustomerID == customer.ID).PhoneNum, 
          Orders = ordersList.Where(order => order.CustomerID == customer.ID),
       };

      carCompanyCustomerList.Add(DTOCustomer);
   }

  return carCompanyCustomerList;
}


Comments

omri said…
Nice post, Gabi. How, would you say, does the normalization level of the database affect the efficiency of your suggested solution?
Gabi Beyo said…
Thanks Omri.
I think that the final suggested design will work better than the first simple design regardless of the DB schema.

The Best

Closures in C# vs JavaScript -
Same But Different

Closure in a Nutshell Closures are a Software phenomenon which exist in several languages, in which methods declared inside other methods (nested methods), capture variables declared inside the outer methods. This behavior makes captured variables available even after the outer method's scope has vanished.

The following pseudo-code demonstrates the simplest sample:
Main() //* Program starts from here { Closures(); } AgeCalculator() { int myAge = 30; return() => { //* Returns the correct answer although AgeCalculator method Scope should have ordinarily disappear return myAge++; }; } Closures() { Func ageCalculator = AgeCalculator(); //* At this point AgeCalculator scopeid cleared, but the captured values keeps to live Log(ageCalculator()); //* Result: 30 Log(ageCalculator()); //* Result: 31 } JavaScript and C# are two languages that support…

Formatting Data with IFormatProvider & ICustomFormatter

This post provides an internal overview of IFormatProvider & ICustomFormatter interfaces, and they way they are handled by .NET.

IFormatProvider is a .NET Framework Interface that should be used, by implementing its single public object GetFormat(Type) method, when there is a need to implement custom formatting of data like String and DateTime.

The public object GetFormat(Type) method simply returns an object that in turns is available to supply all available information to continue the formatting process. The Type passed in by the Framework is meant to give the implementor a way to decide which type to return back. Its like a Factory Method Design Pattern where the "formatType" is the type expected to be returned.
class MyProvider : IFormatProvider { public object GetFormat(Type formatType) { object result = null; //* Factory Method if (formatType == typeof( ICustomFormatter)) //* Some object, will be disc…