How to speed up component frontend development using a product approach: Runiti's experience

Challenges when launching new products

About the product approach

CMF vs CMS

Solution architecture

Implementation of components

Organization of business processes

Preparation for development of business requirements (BT)

BT development

Architecture development

Decomposition and development

Testing

Deployment in a production environment

Operation

Conclusions

Challenges when launching new products

One of the priority areas of my work is the accelerated launch of new products.

By accelerated product launch, I mean a short cycle of setting technical requirements, agreeing on functional and non-functional requirements, development, testing, launch and operation. This is a classic cycle, and the task is to reduce it as much as possible in time.

In the generally accepted approach to solving this kind of problem in the field of website building, it is customary to take a CMS that is suitable for the stack, create a subject area and templates based on it, set up standard integrations and launch the product. To solve popular business problems, this approach is most justified, since we get a working version of the system that has a ready-made architecture, there is a community of developers, and everyone in the labor market knows about it and understands what it is.

It’s another matter if you already have your own system, which is decades old, it consists of different products and a “zoo” of technical solutions, and you need to add one more (or more than one) animal to your “zoo.” The pain of choice is usually more or less the same for everyone – we take the current stack, look for suitable libraries, build an architecture so that it is similar to what was previously done, and implement everything into the general IT landscape. Thus, we maintain the state of the project at the proper level, make changes to the old code as the play progresses, refresh it with new logic, integrations, and everyone should be fine.

However, in practice this is far from the case.

You may have: logic conflicts in business services, layouts and components may be slightly different in a new version of the design, the user experience may change midway, and so on. In fact, the challenge is to make a product from scratch, because it’s faster. But starting from scratch does not mean better, and the problem is that the current technical solution does not allow us to implement our plans.

At this point I would like to distract you and look at the problem from the point of view of the product approach.

About the product approach

A product is a set of functionality and characteristics that create value for a person by satisfying his needs and solving specific problems.

With our business product everything is the same, only it will be used not only by a specific consumer who must buy something on our conditional website, but also by tens or hundreds of people who work in our company. And, often, they will use it many times more often and longer than the end consumer. After all, in the end, websites are a point of interaction between the company and the client, and people use them as needed.

We come to the conclusion that how and what it is made of is not necessary for the user, but for the company itself, because it will ultimately serve this system solution.

As a result, our choice must satisfy all participants in the process – the client, system administrator, manager, developer, testing and operation service.

CMF vs CMS

Choosing a platform is always a difficult question. Some people like how easy and clear it is to write code on it, while others like the web interface provided. In our choice, we decided to find an option that would be as convenient as possible for everyone. We began to study the market and identified several areas for ourselves.

Solution architecture

The architecture of this solution is most easily represented in the form of layers (Fig. 1), since a specific team is responsible for each layer, this will make it easier for us to understand how to manage changes in it.

Figure 1. Layers of the system and the interaction of process participants with them

Figure 1. Layers of the system and the interaction of process participants with them

As can be seen from the diagram, each participant in the system interacts with its own layer and can influence lower layers. This separation is achieved through the implementation of appropriate technical solutions. Management occurs either through visual tools (administration systems in the form of forms and tables, JSON configs), or directly through the application program code.

We chose Git as a repository, so any changes are recorded in the form of a commit, and we can carry out the process of reviewing changes, and we do not require special interfaces for this. GitLab provides everything you need.

Implementation of components

Since the main idea is to highlight components and how to access them, let's look at an example implementation of such a component.

Cart Component

We will use VueJS as the runtime environment.

Project structure

% tree
.
├── cart.vue
├── config.ts
├── events
│   ├── OnItemAdded.ts
│   ├── OnItemAddedConfirmed.ts
│   └── index.ts
└── specs
    ├── events.json
    └── events.yaml

The component file is located in the root cart.vue directory events with generated events, directory specs with OpenAPI and AsyncAPI documentation.

Let's look at an example of a shopping cart component. To implement it with a separate development team we will need:

1) Events that are triggered in parent components (a separate command may be responsible for them).

2) Functional requirements and design (this is all clear).

Events

Separately, we need to consider the mechanism of events inside the component, as well as the consumption of events from outside. In our example, such an event is OnItemAdded.

class OnItemAdded {
   private _userId?: string;
   private _itemId?: string;
   private _quantity?: number;
   private _additionalProperties?: Map<string, any>;
    constructor(input: {
     userId?: string,
     itemId?: string,
     quantity?: number,
     additionalProperties?: Map<string, any>,
   }) {
     this._userId = input.userId;
     this._itemId = input.itemId;
     this._quantity = input.quantity;
     this._additionalProperties = input.additionalProperties;
   }
    /**
    * Идентификатор пользователя, добавившего товар.
    */
   get userId(): string | undefined { return this._userId; }
   set userId(userId: string | undefined) { this._userId = userId; }
    /**
    * Идентификатор добавленного товара.
    */
   get itemId(): string | undefined { return this._itemId; }
   set itemId(itemId: string | undefined) { this._itemId = itemId; }
    /**
    * Количество добавленного товара.
    */
   get quantity(): number | undefined { return this._quantity; }
   set quantity(quantity: number | undefined) { this._quantity = quantity; }
    get additionalProperties(): Map<string, any> | undefined { return this._additionalProperties; }
   set additionalProperties(additionalProperties: Map<string, any> | undefined) { this._additionalProperties = additionalProperties; }
 }
 export default OnItemAdded;

As planned, a separate team implemented a button to add an item to cart, and created the AsyncAPI specification on this topic.

asyncapi: 3.0.0
info:
 title: Корзина товаров
 version: 1.0.0
 description: Спецификация для событий, связанных с добавлением товара в корзину.
channels:
 cart/product-added:
   address: cart/product-added
   messages:
     onItemAdded.message:
       name: onItemAdded
       contentType: application/json
       payload:
         type: object
         $id: onItemAdded
         properties:
           userId:
             type: string
             description: Идентификатор пользователя, добавившего товар.
           itemId:
             type: string
             description: Идентификатор добавленного товара.
           quantity:
             type: integer
             description: Количество добавленного товара.
   description: Событие, когда товар добавляется в корзину.
cart/product-added-confirmed:
   address: cart/product-added-confirmed
   messages:
     onItemAddedConfirmed.message:
       name: onItemAddedConfirmed
       contentType: application/json
       payload:
         type: object
         $id: onItemAddedConfirmed
         properties:
           userId:
             type: string
             description: Идентификатор пользователя, добавившего товар.
           itemId:
             type: string
             description: Идентификатор добавленного товара.
           quantity:
             type: integer
             description: Количество добавленного товара.
           timestamp:
             type: string
             format: date-time
             description: Время, когда товар был добавлен в корзину.
   description: Событие, подтверждающее успешное добавление товара в корзину.
operations:
 onItemAdded:
   action: send
   channel:
     $ref: '#/channels/cart~1product-added'
   summary: Обработка события добавления товара в корзину.
   messages:
     - $ref: '#/channels/cart~1product-added/messages/onItemAdded.message'
 onItemAddedConfirmed:
   action: send
   channel:
     $ref: '#/channels/cart~1product-added-confirmed'
   summary: Обработка события подтверждения добавления товара в корзину.
   messages:
     - $ref: >-
         #/channels/cart~1product-added-confirmed/messages/onItemAddedConfirmed.message
components:
 messages:
   ItemAdded:
     contentType: application/json
     payload:
       type: object
       properties:
         userId:
           type: string
           description: Идентификатор пользователя, добавившего товар.
         itemId:
           type: string
           description: Идентификатор добавленного товара.
         quantity:
           type: integer
           description: Количество добавленного товара.
   ItemAddedConfirmed:
     contentType: application/json
     payload:
       type: object
       properties:
         userId:
           type: string
           description: Идентификатор пользователя, добавившего товар.
         itemId:
           type: string
           description: Идентификатор добавленного товара.
         quantity:
           type: integer
           description: Количество добавленного товара.
         timestamp:
           type: string
           format: date-time
           description: Время, когда товар был добавлен в корзину.

At our component level, to support this specification, we can use a code generator @asyncapi/modelinawhich will generate ready-made code for us from the documentation. In a real project, most likely, these DTOs will be supplied as a single package in order to ensure type consistency within the SPA project. In our example, everything is simplified, and these generated types are stored in a separate directory events.

This approach allows the architecture team to modify event contracts independently of the code and monitor the consistency of data in the project without having to dive into the code base and work only within the specifications. This approach is very similar to the toolkit for GraphQL (for example Apollo Client), in which, after creating the schemas, we generate types and then use them throughout the project.

<template>
   <div>
       <div v-if="items">
           В корзине <i>{{ showCount }}<span v-if="isMaxReached">+</span></i> товар(ов)
       </div>
       <div v-else>
           Ваша корзина пуста
       </div>
   </div>
</template>
import { Parameters, OpenOnPos, useFeaturesConfig } from './config'
import { computed, ref } from 'vue'
import OnItemAdded from './events/OnItemAdded'

const items = ref([])

// Загрузка конфигуграции из админки компонентов
const configuration = useFeaturesConfig<Parameters>({
   MaxItemsInBasketThenPlusSign: 99,
   OpenOn: OpenOnPos.Left,
})

const isMaxReached = computed(() => {
   return configuration.MaxItemsInBasketThenPlusSign > items.value.length
})

const showCount = computed(() => {
   return isMaxReached.value ? configuration.MaxItemsInBasketThenPlusSign : items.value.length
})

function handleOnItemAdded(event: OnItemAdded) {
   // Если в дочернем компоненте добавили товар в корзину, реагируем на это
   items.value.push(event.itemId())
}

Let's now look at the logic of the component itself.

1. We use a hook useFeaturesConfig to load the external config from the widgets admin panel, and set the default parameters for our component. In the example, we set two parameters, one that determines the number of items in the cart when we draw a plus sign, and the second, on which side we will show the list of items in the cart when clicked. For simplicity, the second parameter is not implemented in the component; it is indicated more to demonstrate the capabilities.

2. Next comes the logic of the functions isMaxReached and showCount. Everything is clear with it, if there are more than 99 products, then we show the maximum number and add a plus sign.

3. Subscribe to the OnItemAdded event, which comes from the parent component, and thus monitor the world around us.

Of course, there is another option, to use the global state of the application and do the same through the store, but for ease of presentation I chose the simplest example of such an implementation.

Organization of business processes

Architecture changes have radically changed the roles and order of work on the project; now we need to understand how these changes affected business processes.

Preparation for development of business requirements (BT)

BTs begin with a glossary, or dictionary of terms, that describes the solution. In this dictionary, in addition to the standard description of what we have, we add several new artifacts: “Component”, “Component Configuration”, “Component Markup”, “Component Content”.

At the beginning of the BT, a list of those components that will be changed or created anew is indicated, as well as all its characteristics.

The task is to identify these very components in the visual part of the project at the initial stage, in order to then constantly reuse them and refer to them during the development of the BT. This is labor-intensive, but subsequently, improvements will be greatly simplified, since there will be standardization for the description, functionality and behavior of the component.

BT development

Nothing new. We formalize the business problem using new units, add context and transfer it to the Architecture.

Architecture Development

Since we already have dedicated components, the architect describes the technical interaction between the components using their generic API. Describes the component's data flows and life cycle.

Decomposition and development

The architect's description is ready for implementation, and teams can focus on technical aspects – think through the structure of a new component, or the order of inclusion in an old component. Considering that such things as configuration and markup are already there at the stage of writing the BT, the development team only needs to think through the technical aspect and do engineering without going into the details of business scenarios.

Testing

An application consisting of components can be deployed separately, any configuration can be applied to it, and testing has an out-of-the-box interface for testing edge cases.

Deployment in a production environment

Considering that both the application and the components are in a constant flow of CI/CD, then releasing it to production is no different from rolling it out to a test bench. Upgrade the versions of the application or the packages that come with it, and you're done.

Operation

If you configure the component and application correctly, its appearance and content can be easily managed from the configuration and markup administration system. This can be done by the product manager, without involving development, since the main variables were laid down at the BT stage.

Moreover, you can configure the display on the fly, since the components do not communicate directly with each other, but use an event bus.

Conclusions

Our experiment shows that to speed up development, the only option is to divide the system into different layers, and work on them should be carried out by different teams using agreed processes. These processes are the key to efficiency.

That's all, thanks for your attention 🙂

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *