When I started developing my pet projects, I had a problem with having a consistent system that could help me get a detailed step-by-step guide to implement the project from an idea.
Then I got acquainted with the Software Development Lifecycle (SDLC) and started to implement my own practices. My primary language is Python, and I will tell you the exact workflow of how I usually develop applications in this language.
The SDLC is crucial for creating successful software. It provides a clear plan and structure for each step, helping teams stay organized. Once you start following SDLC, I promise that your projects will become absolutely unrecognizable.
It requires several steps that should be completed, and let’s jump into them immediately:
I am a more practical person than a theoretical one, and I would suggest creating an application on practice applying SDLC principles.
We'll create a "Recipe Recommender" application that suggests recipes based on the ingredients users have.
In order not to make this article too overwhelming, let’s create a simple CLI version, and you will be able to implement the backend and frontend following absolutely the same steps and SDLC approach described in this article.
In the planning phase, the focus is mainly on Gathering Requirements and Setting Estimates.
IMPORTANT: The developer must define precise and well-described requirements. It is crucial to understand the needed functionality and how users will interact with the application. Be as specific as possible!
While setting estimates, I would suggest you break down a big issue into smaller chunks and analyze how much time each small issue will take. Avoid being too strict, and provide your team with a few extra days for development. It’s a rare occasion when everything goes according to the plan in software development 😉.
The design stage is one of the most complicated aspects of the SDLC. Developers should understand that each made decision during this stage can significantly impact the project's success. It contains the following steps:
Usually, in this case, you create a diagram of architecture, and follow the checklist you can see in the screenshot below:
This checklist may vary depending on the technologies being used. But, generally, it is the most abstract representation which can be applicable for the majority of the projects.
For our application we will need the following:
4.2 Low-Level Design:
Break down the project into clear modules or components based on functionality. This stage requires the following:
input_ingredients
, suggest_recipes
, view_recipe_details
.Dictionary
to store recipes and their details.For simplicity, I’ve come up with the following structure:
recipe_recommender/
├── utils.py # user input
├── recipes.py # mini-db and logic of suggestion
└── main.py # put it altogether
4.3 The User Flow
This stage is the most important in my opinion, you just put yourself in the user’s place, and think about how you would interact with an application. Users don’t care about the code quality or the chosen programming language; they want to have a ready-to-use product!
In this part, you bring the idea to life! The most favorite part of programmers. My recommendation will be to follow the best practices known in the world of software engineering during the implementation.
# utils.py
def input_ingredients():
ingredients = input("Enter your available ingredients, separated by commas: ").lower().split(", ")
return ingredients
# recipes.py
recipes = {
"Pasta Salad": {
"ingredients": ["pasta", "tomatoes", "olive oil", "salt"],
"instructions": "Boil pasta, chop tomatoes, mix with olive oil and salt. Serve chilled."
},
"Omelette": {
"ingredients": ["eggs", "salt", "pepper", "cheese"],
"instructions": "Beat eggs, add salt and pepper, cook on a skillet, add cheese before folding."
}
}
def suggest_recipes(available_ingredients):
suggested_recipes = []
for recipe, details in recipes.items():
if all(item in available_ingredients for item in details["ingredients"]):
suggested_recipes.append(recipe)
return suggested_recipes
def view_recipe_details(recipe_name):
if recipe_name in recipes:
print(f"\nRecipe for {recipe_name}:")
print("Ingredients:", ", ".join(recipes[recipe_name]["ingredients"]))
print("Instructions:", recipes[recipe_name]["instructions"])
else:
print("Recipe not found.")
# main.py
from recipes import suggest_recipes, view_recipe_details
from utils import input_ingredients
def main():
while True:
print("\nRecipe Recommender Application")
print("1. Input Ingredients")
print("2. Suggest Recipes")
print("3. View Recipe Details")
print("4. Exit")
choice = input("Choose an option: ")
if choice == '1':
available_ingredients = input_ingredients()
print("Ingredients received.")
elif choice == '2':
suggested_recipes = suggest_recipes(available_ingredients)
if suggested_recipes:
print("Recipes you can make:", ", ".join(suggested_recipes))
else:
print("No recipes found with the given ingredients.")
elif choice == '3':
recipe_name = input("Enter the recipe name: ")
view_recipe_details(recipe_name)
elif choice == '4':
break
else:
print("Invalid choice, please try again.")
if __name__ == "__main__":
main()
During this part, you MUST write tests to ensure that the application works as expected. As well as each piece of new functionality should be tested E2E before moving to the development of new features. The applications must be covered with all types of tests including Unit Testing, Integration Testing, and E2E Testing.
Testing individual functions and modules to ensure they work correctly in isolation.
import unittest
from recipes import suggest_recipes, view_recipe_details
class TestRecipes(unittest.TestCase):
def setUp(self):
self.available_ingredients = ["pasta", "tomatoes", "olive oil", "salt"]
self.missing_ingredients = ["pasta", "tomatoes"]
def test_suggest_recipes(self):
expected_output = ["Pasta Salad"]
self.assertEqual(suggest_recipes(self.available_ingredients), expected_output)
self.assertEqual(suggest_recipes(self.missing_ingredients), [])
Here, you want to ensure that the whole interaction across modules is being consistent and works according to the desired logic.
class TestRecipeRecommender(unittest.TestCase):
@patch('builtins.input', side_effect=['1', 'eggs, cheese, salt', '2', '3', 'Omelette', '4'])
@patch('sys.stdout', new_callable=StringIO)
def test_full_integration(self, mock_stdout, mock_input):
main.main()
output = mock_stdout.getvalue().strip()
self.assertIn("Ingredients received.", output
self.assertIn("Recipes you can make: Omelette", output)
The easiest part of this workflow, act as you are just playing with an app as a user trying to ensure that it works as expected or breaks it.
Recipe Recommender Application
1. Input Ingredients
2. Suggest Recipes
3. View Recipe Details
4. Exit
Choose an option: 1
Enter your available ingredients, separated by commas: pasta, tomatoes, olive oil, salt
Ingredients received.
Try to break your application, identify the pain points, and fix them during the refactoring/maintenance stages. ⬇️
Before the maintenance part, you should deploy your application to make it accessible for the users. I will not focus on this, as it is too huge to cover, but my personal recommendation would be to use services like Heroku/AWS/GCP. The maintenance part is crucial as well; here, we do the following:
This is usually handled by the server providers, and you don’t need to do anything. If your application produces a bad performance, you need to either increase the resources of the server or refactor adding asynchronous functionality. Use profilers such as scalene
and refactor complex logic that eats your resources.
For additional monitoring, it would be nice to set up Grafana or similar tools.
We haven’t added logging during development, and that resulted in problems with tracking how users interact with an application so, it is not possible to track potential bugs and errors in case the application is breaking.
Add the following functions, try catch clauses to the app, and log each crucial section where the interaction happening:
import logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
def log_input(ingredients):
logging.info(f"User input ingredients: {ingredients}")
def log_suggestions(suggestions):
logging.info(f"Suggested recipes: {suggestions}")
def log_error(error_message):
logging.error(f"Error: {error_message}")
log_input(["pasta", "tomatoes", "olive oil", "salt"])
log_suggestions(["Pasta Salad"])
log_error("Recipe not found.")
After implementation of this, you will be able to see if anything went wrong with your app. Fix bugs and respond immediately.
Now, you would like to contact users directly, and ask what they like or don’t like about your application. Ask people to provide feedback, as you have created this software for them. You want to know what functionality people want to be implemented. And how to improve their overall experience with an app.
Suppose we have the following request:
Hi there!
I really like your application! I would like to have an option to add my own recipes to the database and have them included into suggestions!
Best Regards!
Alright, your wish is our command! Let’s start development! And since we are professionals already, we will follow the software development lifecycle! Planning -> Design -> Implementation -> Testing -> Maintenance -> Improvements
Thanks a lot for your attention. I hope that you have got the general idea of developing a high-quality software. As It was mentioned before, I decided to simplify the application in order to have a readable article.
For more complex applications, you need to plan the architecture of high-level components. For instance, how FE and BE interact with each other, the structure of the database, deployment pipelines, and, of course, more complex user flow.
Try the SDLC approach described in this article but be extremely careful, as it can lead to high-quality software 🙂. Best of luck!