The Philosophy of Software Design - The Way We Build at Shipsy
Take a sneak peek into how we solved the Great Problem Decomposition Challenge for everyday development at Shipsy.

Search for a command to run...
Take a sneak peek into how we solved the Great Problem Decomposition Challenge for everyday development at Shipsy.

No comments yet. Be the first to comment.
Shipsy's Dev Team builds ECS-EC2 Sandbox to scale UAT testing and unlocks 70% cost savings.

The engineering team at Shipsy shares a detailed tutorial on automating front-end UI testing with Cypress in a rapid release environment.

Shipsy's Dev team creates a code-free label generator to reduce customer onboarding time and scale the label generation process.

Explore how we steered our mobile app's stability from 68% to 99%, and some hands-on developers' tips to replicate the efforts.

A walk-through of how Shipsy's Routing Playground - a DIY Smart Route Optimizer - came to be.

Shipsy is a SaaS-based smart logistics management platform. We aim at solving the logistics challenges and operational pain points with intuitive and robust software solutions.
Being an agile organization with an intense focus on innovation entails designing and creating future-proof technological solutions that scale, evolve, and stay relevant.
However, delivering across such expectations requires an impeccably robust codebase, which is readable, maintainable, and intuitive for years down the lane.
We aimed at creating a codebase that can serve as a reference for the new developers and “explains itself” even when the original developer is not around. This is also called future-proofing, and here is how we energized the efforts at Shipsy.
Future-proofing has many different faces and definitions based on the business use cases, such as:
In our pursuit, we zeroed down on the fundamentals - problem decomposition, as outlined by the industry stalwart John Ousterhout.
Any computer software is one big problem decomposition challenge - the number of modules, functionalities of these modules, documentation, dependencies, and the overall complexity of the functional system.
As the clients suggest changes or ask for more features, there is hardly the time to play by the rules. So, code refactoring or best practices tend to take the back seat, especially in startups.
Then, how can we make our code future-proof?
System complexities affect developer productivity in many ways, even if it is not apparent from the very beginning. Understanding the working of a piece of code, lots of efforts for implementing small changes, and localizing all the points where a single change reflects - are some symptoms of complex systems.
As the software complexity increases, it becomes more vulnerable to bugs and delays in development.
Software complexities are of three types:
Bad Example:
// file1.js
if (direction === 'UP') {
// ....
}
// file2.js
if (direction === 'UP') {
// ....
}
.....
Good Example:
enum Direction {
UP = ‘UP’,
DOWN = ‘DOWN’,
LEFT = ‘LEFT’,
RIGHT = ‘RIGHT’
}
// file1.js
if (direction === Direction.UP) {
// ....
}
// file2.js
if (direction === Direction.UP) {
// ....
}
As shown in the above example, the shared enum variable holds the direction and each page references that variable.
As shown in the following example, the shared enum variable has the “string” type. However, few files use the “number” type.
enum Direction {
UP = 'UP',
DOWN = 'DOWN',
LEFT = 'LEFT',
RIGHT = 'RIGHT'
}
// file1.js
if (direction === Direction.UP) {
// ....
}
// file2.js
if (direction === Direction.UP) {
// ....
}
// file3.js
// direction holds numbers instead of string
// for e.g up -> 1, down -> -1 left -> -1, right -> 1
if (direction === Direction.UP) {
//
}
Unknown unknowns are the most critical type of complexity, as in this case, you don’t even know that there is an underlying vulnerability or risk in your code.
Generally, in agile organizations, such as startups, software development is tactical, meaning that during development, the main focus is on faster deliveries and accomplishing the intended functionality via shortcuts. As there is no specific focus on long-term goals, there is generally no planning (strategic development) for code refactoring.
Ideally, software development must be strategic, keeping the long-term goals, such as maintainability, in mind. However, the more tactical a developer becomes, the more time they take in developing software, as shown in the following graph:

Hence, we started to look beyond the “working code” and approached code refactoring strategically, such as specific projects that are well matured. This helped us balance the strategic and tactical development goals and steered us towards effortless yet agile system development.
Here are three things that help us reduce system complexities consistently.
Addressing complexities, one at a time.
We create small and independent modules that reduce complexity by ridding dependencies.
However, there is a catch - independent doesn’t mean more modules. We implement independence in our modular design by ensuring that the implementation details in the service class of one module are not visible to another. Also, we use encapsulation so that the service class shows fewer implementation details for layers of abstraction.
Below, we share key considerations for concise and strategic modular design:
Further, the shallow modules tend to be complex because they include functionalities from other modules as well.
Take a look at the following examples for a better understanding.
Bad Example:
export abstract class Inquiry {
abstract createInquiry(organisationId: string, params: CreateInquiryDto);
abstract sendInquiry(organisationId: string, params: SendInquiryDto);
abstract searchInquiry(organisationId: string, params: SearchInquiryDto);
abstract searchByReferenceNumber(organisationId: string, params: SearchInquiryDto);
abstract updateDeadline(organisationId: string, params: UpdateInquiryDeadlineDto);
abstract getInquiryDetailsForShipper(organisationId: string, params: FetchInquiryDetailsDto);
abstract getInquiryDetailsForFF(organisationId: string, params: FetchInquiryDetailsDto);
abstract fetchInquiry(organisationId: string, params: FetchInquiryDetailsDto);
abstract updateInquiry(organisationId: string, params: UpdateInquiryDto);
}
Deep modules work the best for light and clean interfaces, as most of the information is hidden, even if there is a lot of functionality in a method.
Good Example
export abstract class Inquiry {
abstract create(organisationId: string, params: CreateInquiryDto);
abstract send(organisationId: string, params: SendInquiryDto);
abstract search(organisationId: string, params: SearchInquiryDto);
abstract update(organisationId: string, params: UpdateInquiryDto);
abstract fetch(organisationId: string, params: FetchInquiryDetailsDto);
abstract delete(organisationId: string, params: DeleteInquiryDto);
}
Code documentation or commenting is essential to make the code more understandable and readable for future references and correlations.
While good commenting can overcome the unknown unknowns and cognitive load challenges, over-commented code makes things complex.
Below, we share some key pointers to keep in mind for code documentation.
Good comments:
/**
* @summary dump inquiry and bid details to rate_master when bid is sent for an inquiry
* @description
* 1. find port and carrier details to set db internal identifiers
* 2. separate charges with and without container type, size from bid charge details and
* prepare them into rate_master charge format
* 3. check if revised bid is sent
* 4. for each Inquiry container type,size insert/update rate master object
* @param {string} organisationId [Organization identifier]
* @param {Users} user [user details]
* @param {Inquiry} inquiry [inquiry details]
* @param {Bids} bid [bid details]
* @param {OptionsType} options [extra properties]
*/
function addInquiryBidInRateMaster(
organisationId: string,
user: Users,
inquiry: Inquiry,
bid: Bids,
options: OptionsType = {}
) {
}
Bad comments:
// Don’t repeat the code
// get time diff
let timeDiff = currentTime - queryStartTime;
// remove milliseconds
const milliseconds = Math.round(timeDiff % 1000);
timeDiff /= 1000;
const seconds = Math.round(timeDiff % 60);
// remove seconds from the date
timeDiff = Math.floor(timeDiff / 60);
// get minutes
const minutes = Math.round(timeDiff % 60);
// remove minutes from the date
timeDiff = Math.floor(timeDiff / 60);
// get hours
const hours = Math.round(timeDiff % 24);
// remove hours from the date
Every good function or variable name is a combination of three things: Prefix+Action+Context.
Prefix - It is applicable when code is returning boolean values, such as:
is - current state of the context, for example isMember or isAdminhas - current context possesses a certain value, for example hasPermissionshould - positive conditional statement coupled with an action, for example shouldUpdateEntityAction - It is the verb part of your function name, such as:
get - accesses data, for example getStatusCountset - sets variable with value, for example setUserRolereset - reset variable with an initial value, for example resetItemsfetch - requests data usually network requests, for example fetchPostsremove - removes something from somewhere, for example removeFilterdelete - completely remove the existence, for example deletePostcompose - creates new data from existing data, for example composeDisplayAddress(place, area, pincode)handle - handles action, for example handleButtonClickContext - It is the expected data type that a function operates on, such as:
Things to keep in mind during function and variable naming:
Bad Example:
const isExtraServicesSelected = lodash.difference(inquiry.origin.services, originServices).length > 0;
Good Example:
const hasExtraServicesSelected = lodash.difference(inquiry.origin.services, originServices).length > 0;
Bad Example:
const orgModules = orgConfig.modules;
Good Example:
const organisationModules = organisationConfig.modules;
Bad Example:
class country {
this.country_name = ‘’
}
Good Example:
class country {
this.name = ‘’
}
Our previous post on clean coding covers the function and variable naming practices with detailed examples and can be read for a better understanding.
Future-proofing the entire codebase is a gradual and continuous process.
At Shipsy, we created a robust, reliable, and thorough engineering handbook for driving clean coding practices. With a renewed focus on better design and maintainable code, we are moving towards a well-established development organization culture that motivates all of us to work smarter and better.
Explore careers at Shipsy to be a part of our consistently improving and innovation-oriented developer community.
As an effort towards consistent learning and skill development, we have regular “Tech-A-Break” sessions at Shipsy where our team members exchange notes on specific ideas and topics. Contributions: Sahil Arora, Viraj Shah