Skip to main content

Ensuring model association shapes throughout the application stack

· 7 min read

Intro to Json Shapes

We will consider simple

GET /identities/user/:id

For a simple endpoint like getting a single Identities::User, we would expect the response to be:

Response:

{
id: ..
country_id: ..
email: ..
first_name: ..
last_name: ..
phone_code: ..
mobile: ..
}

GET /org/user-profiles/:id

We explore the difference cases of considering associations when getting a single Org::UserProfile.

Response if we do not join tables in the backend:

  • simply rendering the attributes on the model Org::UserProfile
{
id: ...
title: ...
}

Response if I join Identities::User in the backend:

  • Org::UserProfile belongs_to Identities::User
{
id: ..
user: {
id: ..
first_name: ..
last_name: ..
...
}
}

Response if I join all associations of Org::UserProfile in the backend:

  • Org::UserProfile belongs_to Identities::User
  • Org::UserProfile belongs_to Company
{
id: ..
user: {
id: ...
first_name:
last_name:
...
},
company: {
id: ...
}
}

Bad response:

This is a badly designed response because there is no database backed model that follows the following format.

{
id: Org::UserProfile.id,
first_name: Identities::User.first_name,
last_name: Identities::User.last_name,
...
}

The idea is to maintain the json "shape" to follow the backend models.


Problem Statement

For every careers_job_application, I want to know who posted the careers_job

If I add another relationship to careers_job_applications, do I need to join all the tables?

Since AuthProvider wraps the entire frontend application

  • AuthProvider stores Identities::User and Careers::UserProfile and Org::UserProfile (depending where the user logs into)
  • Every page will know who is calling our backend API
  • This means that the backend doesn't need to join the identities user

However, this only applies for only Career::UserProfile cases.

For an Org::UserProfile, they would need to know names of Careers::UserProfile

  • For example, for each Careers::JobApplication, we would want to display the person name, which lives in Identities::User

GET /org-user/careers/job_applications

  • As a org user, I want to see who applied for my job

Looking at the relationship:

The person making this call is an org-user, I need to join the tables to display the name of the user who applied.

I would return the following response:

{
careers_job_applications: [{
id:,
//.. attributes
career_user_profile: {
id: ..,
identities_user: {
id: ..
first_name: ..
last_name: ...
}
}
}]
}

GET /org-user/careers/job_applications/:id (#show)

When showing a single job application:

{
careers_job_applications: {
id:,
//.. attributes
career_user_profile: {
id: ..,
certificates: [{ .. }],
experiences: [{ .. }],
skills: [{ .. }],
identities_user: {
id: ..
first_name: ..
last_name: ..
}
}
}
}

How can we reduce joins to improve performance?

Add a json column career_user_profiles.data, every time we save a Career::UserSomething (i.e. skills, experiences, certificates), we update two tables:

  • Career::UserSomething table
  • Career::UserProfile.data columns
{
careers_job_applications: [{
id:,
//.. attributes
career_user_profile: {
id: ..,
data: { // new column that pre-computes the "join" of the associated tables
certificates: { .. },
experience: { .. },
}
identities_user: {
id: ..
first_name: ..
last_name: ...
}
}
}]
}

The downside of this is that we require to update the tables whenever any one of the Career::UserProfile associations change.

  • then again, it's fairly simple to ensure this by ensuring we call a, for example, Careers::UserProfiles::UpdateDataService in all the places that updates Career::UserProfile associations.

Charlie Question

Imagine we are in a company backend and frontend are two separate teams.

  • I can see that response will always follow the convention where the payload will be correctly formatted (i.e. following the model "shape")
  • can frontend user ignore the shape when they are submitting the form?
    • I imagine frontend does not have visibility how it is implement in the backend

Backend creates endpoint:

  • Send the postman doc to Frontend
  • Frontend would see it and implement their designs
  • Frontend realise in a particular page UI design, the endpoint provided does not return data from a specific model
  • Backend say i cannot join tables any more cause the response you got initially is already joining 10 tables
  • Backend will say, you call this other endpoint to get that model

So what have we established?

  • The default API Response JSON "shape" should follow the model associations.
  • Ideally max up to 3 table joins in the backend

How does this "model association json shape" affect forms in the frontend?

Forms should always follow the model it is rendering for.

If want to create a Car, I would have a CarForm that follows the Car attributes.

  • one form field for each Car attribute.

Likewise, if want to create a Org::UserProfile, I would have a OrgUserProfileForm that follows the Org::UserProfile attributes.

Org::UserProfile Attributes

  • id
  • identities_user (association)
  • company (association)
  • title
  • created_at
  • updated_at

In my <OrgUserProfileForm>, I would need fields for the associations as well.

Scenario 1: Admin

Assume Admin (jod staff) is creating an Org::UserProfile.

  • Admin can access everything in the system
    • they can see all identities_users and companies
<Form>
<TextInputField name={'title'}>
<IdentitiesUserSelectField name={'identities_user.id'}>
<CompanySelectField name={'company'}>
</Form>
const IdentitiesUserSelectField = () => {
// makes an api call to GET /admin-user/identities/users
// gets a list of identities
// render the list of identities in a dropdown
// user select 1 identity from the response
// update the form OrgUserProfileForm state via react-hook-form
}

When I select a Identities::User and Company from their respective select fields

  • my form state now has the Identities::User attributes and company attributes.
  • this means I can nicely render the models int he frontend after the user selects.

form-state-with-ui.drawio.svg

Scenario 2: Org User

Assume Org User is creating an Org::UserProfile.

  • Org User can only access everything associated with their company
  • They can only see Identities::User and a single Company, which is the company they belong to.

Thinking about the form schema for OrgUserProfileForm

note

In the frontend, form is named as {ModelName}Form

const orgUserProfileSchema = () => {
title: zod.string()
// company_id: zod.integer()
company: {
id: zod.integer().required()
}
identities_user: {
id: zod.integer()
}
}

const OrgUserProfileForm = () => {
return (
<Form>
<TextInputField name={'title'}/>
<TextInputField name={'identities_user.first_name}/>
<Form/>
)
}

Yuri Question

Job Application <-- we need to insert this
- User Career Profiles <-- we don't need to insert anything in this one
- User Career Experience <-- we need to insert this
note

While the question does not make sense in our domain, consider the same associations in the models.

I want to create a Careers::JobApplication

  • and at the same time create Career::UserExperience

Looking at the model associations (not the association on the database level)

TL;DR: Yes we follow the format.

// form schema follow "shape"
const jobApplicationSchema = {
careers_job: {
id: 1,
},
careers_user_profile: {
id: 1 // we need to know who to create the Careers::UserExperience for
careers_experience: [{
job_title: ..
}]
},
}

// Alternative form schema which does not consider "model association json-shape"
// While it's not wrong from a technical perspective, this is not our convention.
const jobApplicationSchema = {
careers_job_id:
careers_user_profile_id:
careers_experiences: [{ .. }, ...]
}

Imam Highlight:

Whenever you change any form schema/state shape, ensure you are assigning errors to the correct form field names.

  • this will ensure that errors are rendered on the correct field in the form

Naming things


July Direction

Week 1 we will make changes to existing MVP based on the technical feedback from ali

  • naming
  • api layer should not have async
  • form schema should follow endpoint structure/shape
  • endpoint should follow shape of model associations

Week 2

  • start on the peripheral services
    • send email
    • simple notification system
    • event/analytics
    • file upload

Week 3

  • continue with peripherals
  • start on product feedback

Week 4

  • continue on product feedback