1. Introduction 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. 2. Application Overview 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. 3. Planning In the planning phase, the focus is mainly on Gathering Requirements and Setting Estimates. 3.1 Gathering Requirements: Users need to input available ingredients. The application should suggest recipes that can be made with those ingredients. Users should be able to view recipe details including ingredients and instructions. 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! 3.2 Setting Estimates: 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 😉. Development time: 2 weeks. Testing time: 3 days. 4. Design 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: High-Level Design (System Architecture): Low-Level Design (Component and Module Details): User-Flow (How the user interacts with an application) 4.1 High-Level Design 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: A script with functions for ingredient input, recipe suggestions, and displaying recipe details. A simple database (dictionary) of recipes. 4.2 Low-Level Design: Break down the project into clear modules or components based on functionality. This stage requires the following: The structure of the project. Functions: input_ingredients, suggest_recipes, view_recipe_details. Data Storage: 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! 5. Implementation 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() 6. Testing 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. 6.1 Unit 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), []) 6.2 Integration Testing 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) 6.3 E2E Testing 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. ⬇️ 7. Maintenance 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: 7.1 Performance and Usage 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. 7.2 Logging 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. 7.3 User Feedback 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 8. Conclusion 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! 1. Introduction 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: 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. 2. Application Overview We'll create a "Recipe Recommender" application that suggests recipes based on the ingredients users have. "Recipe Recommender" 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. 3. Planning 3. In the planning phase, the focus is mainly on Gathering Requirements and Setting Estimates. Gathering Requirements Setting Estimates. 3.1 Gathering Requirements: 3.1 Gathering Requirements: Users need to input available ingredients. The application should suggest recipes that can be made with those ingredients. Users should be able to view recipe details including ingredients and instructions. Users need to input available ingredients. The application should suggest recipes that can be made with those ingredients. Users should be able to view recipe details including ingredients and instructions. 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! IMPORTANT: 3.2 Setting Estimates: 3.2 Setting Estimates: 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 😉. Development time: 2 weeks. Testing time: 3 days. Development time: 2 weeks. Testing time: 3 days. 4. Design 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: High-Level Design (System Architecture): Low-Level Design (Component and Module Details): User-Flow (How the user interacts with an application) High-Level Design (System Architecture): High-Level Design Low-Level Design (Component and Module Details): Low-Level Design User-Flow (How the user interacts with an application) User-Flow 4.1 High-Level Design 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: A script with functions for ingredient input, recipe suggestions, and displaying recipe details. A script with functions for ingredient input, recipe suggestions, and displaying recipe details. A simple database (dictionary) of recipes. A simple database (dictionary) of recipes. 4.2 Low-Level Design: 4.2 Low-Level Design: Break down the project into clear modules or components based on functionality. This stage requires the following: The structure of the project. The structure of the project. Functions: input_ingredients, suggest_recipes, view_recipe_details. Functions: input_ingredients , suggest_recipes , view_recipe_details . input_ingredients suggest_recipes view_recipe_details Data Storage: Dictionary to store recipes and their details. Data Storage: Dictionary to store recipes and their details. Dictionary 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 recipe_recommender/ ├── utils.py # user input ├── recipes.py # mini-db and logic of suggestion └── main.py # put it altogether 4.3 The User Flow 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! 5. Implementation 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 # 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.") # 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() # 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() 6. Testing 6. Testing 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. MUST write tests Unit Testing Integration Testing, and E2E Testing. 6.1 Unit 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), []) 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), []) 6.2 Integration Testing 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) 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) 6.3 E2E Testing 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. 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. ⬇️ refactoring/maintenance 7. Maintenance 7. Maintenance 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: 7.1 Performance and Usage 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. scalene For additional monitoring, it would be nice to set up Grafana or similar tools. 7.2 Logging 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: 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.") 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. Fix bugs and respond immediately. 7.3 User Feedback 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. you have created this software for them 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! 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 Planning -> Design -> Implementation -> Testing -> Maintenance -> Improvements 8. Conclusion 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! be extremely careful, as it can lead to high-quality software