Why
Modern UIs often implement offline capabilities and/or follow the optimistic ui pattern. In these cases you'd need to handle new entities in your client side state before a server response (which traditionally contains the ID) is available. To support this, it is helpful to create IDs on the client side.
When you create IDs client side, you could follow different setups:
- Traditional Server Server does not know about any client side IDs. Client "merges" server generated IDs.
- Client only Server does not generate IDs but expects (all) clients to provide IDs. Normally used with UUIDs.
-
Server side mapping Clients propagate their generated ID to the server, but the server generates new ID and keeps/creates (a mapping of) both IDs (e.g. adding an attribute
client_id
to the entity).
Even though all setups are sensible (depending on the specific context), I'd like to highlight that only "Traditional Server" can be used for scenarios where you have no control over the server/API (or do not want to change these).
The following sections will focus on the Traditional Server setup and document some best practices. Please reach out if something seems incorrect or missing!
Keep temporary ID around
Even after the server supplied the (persistent) ID, it is often helpful to keep the corresponding temporary ID around - at least until the entity is removed from the client state.
For example this is useful when tracking list/table entries (e.g. with a trackBy
function in Angular):
trackBy = (item, index) => item.temporaryId ?? item.id;
The above can only continuously identify the object, if you do not remove the temporaryId
(even once the id
is present).
Note: If you use the temporary ID in your URL, you should replace the URL with the persistent ID as soon as possible to make sure on a page refresh (when client state is generally reset) it is possible to re-fetch the entity from the server.
Use a separate attribute
You can put the client generated ID into the same attribute as the server generated ID. In this case it is advisable to clearly distinguish client/local IDs and server IDs - e.g. by prefix or by using negative numbers.
Generally it is easier to maintain your code if the client/local ID is put into a separate attribute - e.g. temporaryId
. Not only does this enable to "Keep temporary ID around" (see above), but it is also less error prone, as you cannot mistakenly mix up client and server IDs.
In Typescript you can easily define an interface
interface HasTempId {
temporaryId?: string;
}
and join this into your models:
const entity: YourEntity & HasTempId = {/* whatever */};
This makes it easy, to retrospectively add client side generated IDs to already existing code.
Admittingly, this is not as easy for nominally typed languages such as Java.
Referential Consistency
When entities reference other entities, these references should be taken into account. In general it is not necessary to create additional properties to hold temporary references, but you can first put the temporary/client ID into the original reference attribute and replace it with the persistent ID, once it is available. Of course, you should take care that all references of all relevant entities are updated.
This works naturally with central state patterns, such as Redux.
Stable order
If you have sub-resources modeled as a list that have IDs on their own (UML "Aggregation") you need to match all elements. To correctly associate client side IDs with server side IDs, the easiest way is to guarantee the order of this list (server) and just associate elements by their index.
In case the former is not possible you can still identify elements by their structure/values. Note that if you cannot distinguish elements, the correct order does not matter.
Conclusion
Generating and maintaining client side temporary IDs does add some complexity. A structured approach and using a modern (Web) stack with Typescript and a centralized state management helps keeping the solution maintainable.
Top comments (0)