During my career, I have developed and redeveloped dozens of large systems from scratch.
The business domains were vastly different from each other: a nationwide electronic library, a federal accreditation system for chemical laboratories, online stores including the largest B2B adult products store in the CIS, a plant growing automation system, crypto payment gateways, several chat applications, telemetry and automation for city parking, and so on.
Each time I encountered a new system, the main question arose: "Where do I start the design?"
And I found an answer that works in 100 out of 100 cases:
Start system design with Processes
How it works
In abstract terms:
- Absolutely any code solves a task, so first we need to figure out "what result we want to achieve," that is, describe the Tasks
- Next, we describe the Processes – the sequence of actions that we must perform to solve the Task.
- Then we go through the description of all Processes and can extract Data Structures, dividing them in a way that will respond well to the Processes.
- The next step is to identify steps that are repeated in Processes and extract them into Operations.
Let's look at the example of developing an online store:
i. Tasks
- The client should be able to:
- Search for products
- Add them to the cart
- Place an order
- View order history
- Change data of an order that hasn't been collected yet
- Receive notifications about order status changes
- Register and Authenticate
- Admin should be able to:
- Add products
- See customer orders
- Change order data
- Change order status
ii. Processes
- Client
- Place an order
- View the list of products in my cart
- Input (I): user identifier
- Output (O): Cart
- See the sum, discounts, promotions, delivery conditions
- I: Cart identifier, Delivery Condition identifier
- O: original amount, discount size, delivery cost, total amount
- Pay for the order
- I: Cart identifier
- O: link to the payment system
- View the completed order
- I: Order id
- O: Order data
- Admin
- Add product
- Add product description
- I: description
- O: OK
- Cost
- I: number
- O: OK
- Delivery features
- I: set of delivery condition identifiers
- O: OK
- Set categories and tags
- I: set of category and tag identifiers
- O: OK
- Add it to promotions
- I: set of promotion identifiers
- O: OK
- Describe which discount policy it belongs to
- I: set of discount policy identifiers
- O: OK
iii. Data Structures
Now let's form the conditional Data Structures needed for these processes:
- User – User or Admin
- id
- Product
- id
- name
- price
- Cart
- id
- productIds
- userId
- Order
- id
- productIds
- userId
- status
- addressId
- deliveryTypeId
- Receipt
- id
- status
- orderId
- User Address
- id
- city
- street
- userId
- Delivery Conditions
- id
- name
- prices
- Payment Type
- id
- name
- Promotion
- id
- name
- productIds
- priceCoefficient
- Discount Policy
- id
- name
- productIds
- userIds
- priceCoefficient
I want to note that these structures currently only respond to the two processes that I described in the previous section. The more processes there would be, the more data structures I would have described.
- since we don't have OOP, I don't need to make them according to some principle of combining behavior and data. No, I describe them in a way that will be convenient for me to store them in the database. That is, if it would be convenient to store an Order in pieces in 3 different databases, then I would have 3 different data structures describing how it is stored in these databases.
iv. Operations
The last stage is to find procedures that are repeated between processes and extract them into Operations:
- Change order data – logic that is available to both Admin and User, the only difference being that the admin has access to this change in any Order status, and the User only in the "Formed" status.
This is a very simplified example, but the most important thing I want to show is how cool and easy such design is applied to code:
// The main difference from the design stage is that we first // describe Data Structures, then Processes, then Operations // # Data Structures // ./data/main-db-data-structures.ts type User = { id: string; role: "client" | "admin" } type Product = { id: string; name: string; price: number; } type Cart = { id: string; productIds: string[]; userId: string; } type Order = { id: string; productIds: string[] userId: string; status: string; addressId: string; deliveryTypeId: string; } type Check = { id: string; status: string; orderId: string; } type UserAddress = { id: string; city: string; street: string; userId: string; } type DeliveryType = { id: string; name: string; prices: number[] } type PaymentType = { id: string; name: string; } type Stock = { id: string; name: string; productIds: string[] priceCoeficient: number; } type DiscountPolicy = { id: string; name: string; productIds: string[] userIds: string[] priceCoeficient: number; } // # Processes, with description of Input and Output Data Structures // ./process/get-my-cart.ts const getMyCart = (userId: string): Cart = { return db.cart.findOne({ userId: userID }) } // ./process/calculate-cart-sum.ts // this type belongs only to the Process below, so it will be described // next to it type CalculateCartSumResult = { sumPrice: number deliveryPrice: number totalPrice: number } const calculateCartSum = (cartId: string, deliveryTypeId: string): CalculateCartSumResult => { const carts = db.cart.findOne({ userId: userID }) // Get Products const products = db.products.find({ id: carts.productIds }) // Get related promotions and discounts const stocks = db.stock.find({ productIds: products.map(p => p.id) }) const discountPolicies = db.discountPolicy.find({ productIds: products.map(p => p.id) }) // Get delivery type const deliveryType = db.deliveryType.find({id: deliveryTypeId}) // And here are some cost calculations return { sumPrice: ..., deliveryPrice: ..., totalPrice: ..., } } // ./process/confirm-order-ts const confirmOrder = ( cartId: string, deliveryTypeId: string, paymentTypeId: string ) => { // ... } // and so on for all Processes // ... // # Now let's describe Operations that can be reused by Processes // ./opertaions/change-order-info.ts const changeOrderInfo = (order: Order, byWhom: user, newOrerData: Partial<Order>): void => { // Prohibit changing order information if it is not in "formed" status and // the Client wants to change it if (order.status !== "formed" && user.role === "client") return; // If everything is ok, then we do all the necessary transformations // ... }
Notice:
- I don't combine Processes into any "Services" and the like.At most, I might combine them into some module or microservice by domain (for example, in one place, everything related to Cart, Products, and so on; in another, everything related to Promotions; in a third, Payments), which would affect which folders the functions of these Processes would be in.
- Each Process and Operation can use absolutely any Data
There are many more design and application building rules, we will talk about them in other chapters and even books.
The main thing I want to convey: design starting with Processes, and write your code starting with Functions.
This is the best way to meet the system requirements and make it flexible enough for further development.
This is what is actively used in both FP, PP, and, in fact, FOP.
What's next
Ok, we'll start with processes, but what is the right approach to working with Data?
That's what the next chapter is about:
👈 Previous chapter
Next chapter 👉