Today's essay deals with the tricky issue of custom features for individual customers who are running instances of your software.
The question comes by way of a regular reader who prefers to remain anonymous, but asks this:
... I work on a large (to me, anyway) application that serves as a client database, ticket system, time-tracking, billing, asset-tracking system. We have some customers using their own instances of the software. Often, those customers want additional fields put in different places (e.g., a priority column on tickets). This results in having multiple branches to account for versions with slight changes in code and in the database. This makes things painful and time-consuming in the long run: applying commits from master to the other branches requires testing on every branch; same with database migrate scripts, which frequently have to be modified.
Is there an easier way? I have thought about the possibility of making things "optional" in the database, such as a column on a table, and hiding its existence in the code when it's not "enabled." This would have the benefit of a single code set and a single database schema, but I think it might lead to more dependence on the code and less on the database -- for example, it might mean constraints and keys couldn't be used in certain cases.
Restating the Question
Our reader asks, is it better to have different code branches or to try to keep a lot of potentially conflicting and optional items mixed in together?
Well, the wisdom of the ages is to maintain a single code branch, including the database schema. I tried exactly once, very early in my career, to fork my own code, and gave up almost within days. When I went to work in larger shops I always arrived in a situation where the decision had already been made to maintain a single branch. Funny thing, since most programmers cannot agree on the color of the sky when they're staring out the window, this is the only decision I have ever seen maintained with absolute unanimity no matter how many difficulties came out of it.
There is some simple arithmetic as to why this is so. If you have single feature for a customer that is giving you a headache, and you fork the code, you now have to update both code branches for every change plus regression test them both, including the feature that caused the headache. But if you keep them combined you only have the one headache feature to deal with. That's why people keep them together.
Two Steps
Making custom features work smoothly is a two-step process. The first step is arguably more difficult than the second, but the second step is absolutely crucial if you have business logic tied to the feature.
Most programmers when confronted with this situation will attempt to make various features optional. I consider this to be a mistake because it complicates code, especially when we get to step 2. By far the better solution is to make features ignorable by anybody who does not want them.
The wonderful thing about ingorable features is they tend to eliminate the problems with apparently conflicting features. If you can rig the features so anybody can use either or both, you've eliminated the conflict.
Step 1: The Schema
As mentioned above, the first step is arguably more difficult than the second, because it may involve casting requirements differently than they are presented.
For example, our reader asks about a priority column on tickets, asked for by only one customer. This may seem like a conflict because nobody else wants it, but we can dissolve the conflict when we make the feature ignorable. The first step involves doing this at the database or schema level.
But first we should mention that the UI is easy, we might have a control panel where we can make fields invisible. Or maybe our users just ignore the fields they are not interested in. Either way works.
The problem is in the database. If the values for priority come from a lookup table, which they should, then we have a foreign key, and we have a problem if we try to ignore it:
- We can allow nulls in the foreign key, which is fine for the people ignoring it, but
- This means the people who require it can end up with tickets that have no priority because it does not prevent a user from leaving it blank.
A simple answer here is to pre-populate your priority lookup table with a value of "Not applicable", perhaps with a hardcoded id of zero. Then we set the default value for the TICKET.priority to zero. This means people can safely ignore it because it will always be valid.
Then, for the customer who paid for it, we just go in after the install and delete the default entry. It's a one-time operation, not even worth writing a script for, and it forces them to create a set of priorities before using the system. Further, by leaving the default of zero in there, it forces valid answers because users will be dinged with an FK violation if they do not provide a real priority.
For this particular example, there is no step 2, because the problem is completely solved at the schema level. To see how to work with step 2, I will make up an example of my own.
Step 2: Unconditional Business Logic
To illustrate step 2, I'm going to make up an example that is not really appropriate to our reader's question, frankly because I cannot think of one for that situation.
Let's say we have an eCommerce system, and one of our sites wants customer-level discounts based on customer groups, while another wants discounts based on volume of order -- the more you buy, the deeper the discount. At this point most programmers start shouting in the meeting, "We'll make them optional!" Big mistake, because it makes for lots of work. Instead we will make them ignorable.
Step 1 is to make ignorable features in the schema. Our common code base contains a table of customer groups with a discount percent, and in the customers table we make a nullable foreign key to the customer groups table. If anybody wants to use it, great, and if they want to ignore it, that's also fine. We do the same thing with a table of discount amounts, we make an empty table that lists threshhold amounts and discount percents. If anybody wants to use it they fill it in, everybody else leaves it blank.
Now for the business logic, the calculations of these two discounts. The crucial idea here is not to make up conditional logic that tries to figure out whether or not to apply the discounts. It is vastly easier to always apply both discounts, with the discounts coming out zero for those users who have ignored the features.
So for the customer discount, if the customer's entry for customer group is null, it will not match to any discount, and you treat this as zero. Same for the sale amount discount, the lookup to see which sale amount they qualify doesn't find anything because the table is empty, so it treats it as zero.
So the real trick at the business logic level is not to figure out which feature to use, which leads to complicatec conditionals that always end up conflicting with each other, but to always use all features and code them so they have no effect when they are being ignored.
Conclusion
Once upon a time almost everybody coding for a living dealt with these situations -- we all wrote code that was going to ship off to live at our customer's site. Nowadays this is less common, but for those of us dealing with it it is a big deal.
The wisdom of the ages is to maintain a common code base. The method suggested here takes that idea to its most complete implementation, a totally common code base in which all features are active all of the time, with no conditionals or optional features (except perhaps in the UI and on printed reports), and with schema and business logic set up so that features that are being ignored simply have no effect on the user.
96 comments:
Of course, one has to be careful that new features do have zero effect when being ignored. The repeated pattern I've encountered over the years is 'why has this batch process gone up by 25%' - and it turns out to be because it's doing a simple query that does nothing . . . 500,000 times.
(Back to the problems of encapsulation vs optimising loops).
Also, when it comes to UI design, my preference is for either leaving the field there (to be ignored) or greyed out if disabled. Actual invisibility either leaves odd gaps in a layout, or some kind of inevitably inferior auto-layout.
(The big exception is columns in a data grid)
Should add that the 'do nothing 500,000 times' example isn't a DBMS problem. The query itself could take 0 msecs, it's the 500,000 IPC or network calls that are the problem!
Ken, I gotta say I like your solution enough to try it out next time I'm on that situation.
I do have two comments though regarding the two alternatives.
I think maintaining separate branches is becoming easier with modern SCM tools and dev practices. At the very least, good merge tools, continuous integration, and good unit test coverage (in terms of meaningful coverage, not just running through all your branches) make the process more mechanical.
With regards to custom db deploy scripts, who writes deploy scripts manually anymore? On the Microsoft side of things, higher end versions of Visual Studio, or Redgate SQL compare will let you merge schemas.
Yes, sometimes you have to edit the script, especially if there is a need to do some DML, I personally find that to be rare in practice. I am very "write it by hand" when it comes to DDL scripts on the dev machine, but I just don't see the sense in doing subsequent deploys towards production.
"Code that was going to ship off to live at our customer's site" has become code that is going to live at a single-tenant SaaS app. But the principles are the same.
We are using one table which is used to switch off or switch on different functionality. Also have one code base, without branching. Maintain "regular" code, from my point of view, is different than maintain database code. There is one more variable which do not exists in "regular" code, and exists in databases. This is time. And data, as a consequence of time. It means that it is possible to get latest version of "regular" code, and deliver it, but database should be upgraded(I regard here schema and data changes). So, the same mechanism can not be applied.
> There is some simple arithmetic as to why this is so. If you have single feature for a customer that is giving you a headache, and you fork the code, you now have to update both code branches for every change plus regression test them both, including the feature that caused the headache. But if you keep them combined you only have the one headache feature to deal with. That's why people keep them together.
I debated posting a reply to this, and took some time to mull over exactly what I wanted to say.
First, you are right. At the surface, it is simple arithmetic.
But the underlying problem of having a feature implemented two different ways for two separate clients still exists, and gets worse as the number of clients approaches infinity. Computer scientists specializing in software engineering have a name for the subfield dedicated to studying this problem: Software Factories and Software Product Lines. And these things can get very complicated. For example, ontological mapping becomes an issue. If you use the word "contract number" with the wrong client, then they will give you an insurance contract number, rather than the global insurance plan id. Or vice versa: Use the word "insurance plan" and they give you contract level details instead. It all depends on how their accounting subsystem was set-up. These sorts of ontological issues were covered in William Kent's fantastic book, Data and Reality (1977).
Now, for a more practical view:
I think if you have a multi-tenant database and many clients, and in that database you have a table(s) with sparsely populated columns representing what features each client has flipped on, then you are begging for trouble. The pricing of your software simply will not work unless it is ridiculously expensive. I may be mistaken, but this is what happens with most shrinkwrapped and semi-extensible ERP/CRM solutions like Peoplesoft, SAP, Meditech, Epic, VPM, and so on.
In your Step 1 section, you said,
Further, by leaving the default of zero in there, it forces valid answers because users will be dinged with an FK violation if they do not provide a real priority.
Are you sure you didn't mean that if you delete the default it forces them to provide a real priority? Because if you leave the default, that customer doesn't have to provide a priority, wasn't that the point of the default?
thats one of the best article for us in the domain
The questioner says this happens 'often' so maybe they should consider refactoring the system to use an EAV db schema.
Please visit my blog, I'll be publishing C++ tutorials during the summer! http://programming-blog.com
Thanks! :)
a good concept. ignore the redundancy ... please visit more database concept www.database-concept.com
I have actually gone the other way to some extent, and seen the db as a platform where functionality can be added. Although I think your approach works very well in many cases, there are cases (like ERP systems) where you are essentially shipping a framework, and that framework may have a lot of features very few customers need.
In these cases, it makes sense to follow something like your advice but also modularize so you dont have to ship the whole product to everyone.
hey ken,
I havent spoke to you in a while. I met you through donald Organ and tom melendez. you had worked on my site 'circuits" a few years back. I need a little advice as i move forward with some stuff and i figured you would be the guy that could point me in the right direction..anyway if you get a few minutes to spare could you shoot me an email with your contact info on it. I appreciate it. thank you..
john jedi4425@gmail.com
hey ken,
i havent spoken to you in a while about programming. i was introduced to you by donald organ and tom melendez...you worked on "circuits" for me a few years back. I was wondering if you could shoot me an email when you get a quick chance. i could use a little advice as i proceed with my project..thanx john
jedi4425@gmail.com
cara mengatasi berbagai macam penyakit secara alami
Obat Herbal Penurun Darah Tinggi Obat Herbal Ambeien Akut Obat Herbal Batu Empedu Tanpa Operasi Obat Herbal Sinusitis Kronis Obat Herbal Tumor Otak Jinak Obat Herbal Wasir Kronis Tanpa Operasi Obat Herbal Liver Kronis Paling Ampuh Obat Herbal Bronkitis Kronis Obat Herbal Usus Buntu Tanpa Operasi Obat Herbal Eksim Paling Ampuh Obat Herbal Gagal Ginjal Kronis
pengobatan penyakit glaukoma secara alami tanpa operasi
Obat Herbal Glaukoma
your post is very interesting
thank you for sharing
i really liked
Obat Herbal TBC
excellant post
Obat Herbal Polip Hidung
Hi,
its a very helpful information.
A well-managed email database is by far the best way to boost sales without risking budget waste because it appeals to consumers who have already opted in to receive marketing information from you. Germany Mailing Lists
I would like to appreciate this nice article. I am waiting for this kind of informative interesting blog post. You ma find best poker table : Custom Poker Table
This is very nice article. When i was searching for primary care doctor in Bangalore i got The Family Doctor centre. It is also a best hospital in Bangalore. For more information please go through the link
gclub casino online
casino online
gclub online
Hi sir,
This article is very much helpful. But still I have question on it should I ask here ?
Wow..!! This is an adorable post as the information shared in this post is very helpful. I am thankful to get this post when I was searching a web developer and mobile app developer
Inti dari permainan ini adalah, jeli membaca kondisi dan kartu. Yang paling jeli, dialah yang akan keluar sebagai pemenang. Keberuntungan tidak lah menjadi faktor utama lho dalam permainan ini
asikqq
pionpoker
dewaqq
bandar ceme
sumoqq
hobiqq
paito warna
interqq
forum prediksi
Are you tired of being human, having talented brain turning to a vampire in a good posture in ten minutes, Do you want to have power and influence over others, To be charming and desirable, To have wealth, health, without delaying in a good human posture and becoming an immortal? If yes, these your chance. It's a world of vampire where life get easier,We have made so many persons vampires and have turned them rich, You will assured long life and prosperity, You shall be made to be very sensitive to mental alertness, Stronger and also very fast, You will not be restricted to walking at night only even at the very middle of broad day light you will be made to walk, This is an opportunity to have the human vampire virus to perform in a good posture. If you are interested contact us on Vampirelord7878@gmail.com
Yws
Complete the Manual setup of Roku Streaming Device and once you complete the process go for the given steps on your system or mobile.
Steps to be done on the system
1.Now, open the system and click on the browser of your choice
2.Navigate to the Roku.com/link
3.You will find the space to type the code
4.Subsequently, type the alphanumeric code on the space and hit on the submit overlay
5.This should activate your Roku device
For any further queries on Roku com link Activation, feel free to contact our active customer care team, working round the clock at the toll-free number.
best makeup artist in gurgaon
best makeup academy in gurgaon
Www.webroot.com/safe
Online Download Www.webroot.com/safe
Install Www.Webroot.Com/Safe With Key Code
Webroot.com/safe
What exactly is Putlocker?
If you've made it so far without perceiving what it is, here's a straightforward rundown of what Putlocker is: Putlocker is an online stage that allows you the opportunity to stream movies and shows television to no end.
No sign up, no Mastercard, that's needed straightforward. Essentially visit the site, type what you need to look for and you're good to go. It's one of several Putlocker hd
Beginning late, Putlocker can be known as GoMovies. The two destinations are in any case essentially the proportionate, they simply use a substitute zone and name.
How does Putlocker do its work?
Close. Close. Rather than promoting the video recording itself, what Putlocker is doing is going on as a step that allows you access to accounts from well-known cyberlockers around the world. Significant
Note: The default regions for Dragon User Profiles are recorded early.
Duplicate the facilitator of the User Profile.
Note: The facilitator name will be the name of the User Profile.
Glue the User Profile facilitator to another region, (for example, the Windows Desktop) to trade that User
nuance dragon technical support.
To genuinely import a User Profile:
Open a Windows Explorer window.
Break down to the record where the Dragon User Profile(s) were emphasized to.
Duplicate the organizer of the User Profile.
Note: The organizer name will be the name of the User Profile.
Break down to the territory where the Dragon User Profiles are overseen.
Note: The default regions for Dragon User Profiles are recorded early.
Glue the User Profile facilitator to this region to import that User Profile
Thanks for your ideas. You can also find the details on Affity Solutions
HINDISONGS
putlocker one of the great streaming systems, in which you can watch special films and tv series. Moreover, you may down load various motion pictures of your choice and watch it while you want to observe it subsequent.
At a time, the channels have been close down and plenty of humans had been no longer able to watch the favorite films in their desire. Because of the lack of ability to benefit get right of entry to to their internet site, many human beings have to look for the alternative. Incidentally, there are plenty of alternatives available for you.
You can test the numerous alternatives supplied to you here and use any of them to watch movies on putlockers , tv series, and most significantly, you can down load films of your preference.
You can watch on line movies and also you should make certain that you are secure while watching it. To be safe way that the film website will now not be harm on your machine with viruses. This is one of the risks users encounter in the sort of web page. The possibility of encountering viruses is constantly there.
Before you start to get right of entry to that site, you must make sure that you do not disclose your PC or your telephone or any of the devices you want to watch the video. If you need to download movies, you ought to additionally use sturdy antivirus to ensure which you do no longer down load any film that can damage your machine.
This may be very vital, specifically in case you want to benefit get right of entry to to a free website like maximum of them which might be encouraged here.
Furthermore, to make certain which you live safe, you ought to make certain that you examine the website's guidelines and do now not cross contra to the regulations put in area. Ensure that you do not destroy any rule you can get prosecuted in case you go towards the policies.
Putlocker is an online stage that allows you the opportunity to stream movies and shows television to no end.
UPLAY365
This is a very good content and thank you for sharing such an wonderful information. Please follow my website for more information in Advanced SEO Training in Kolkata.
Digital Marketing Training.
Digital Marketing training institute.
Digital Marketing Agency in Kolkata.
Digital Marketing Services in Kolkata.
Advanced Digital Marketing Courses.
SMM Training in Kolkata.
SEO Training in Kolkata.
Excellent Content. I am following your blog on a regular basis. Thank you for sharing this. Please follow my website for more information in Mobile Repairing Training Course.
Mobile Repairing Training in Kolkata.
Bike Repairing Training in Kolkata.
TV Repairing Training in Kolkata.
AC Repairing Training in Kolkata.
Computer Repairing Training in Kolkata.
AC Repairing Training in Kolkata.
CCTV Repairing Training in Kolkata.
Very nice information. Really appreaciated. Thank you for the details. Please follow my site for PLC Training in Kolkata.
PLC SCADA Training in Kolkata.
PLC Training in Kolkata.
SCADA Training in Kolkata.
HMI Training in Kolkata.
VFD Training in Kolkata.
Industrial Automation Training in Kolkata.
Please keep sharing this types of content, really amazing. Please follow my website for more information in Best IT Professional Courses in Kolkata.
ANSIBLE Training in Kolkata.
DevOps Training in Kolkata.
CKA Training in Kolkata.
CKAD Training in Kolkata.
Docker Training in Kolkata.
Kubernetes Training in Kolkata.
AWS Training in Kolkata.
AZURE Training in Kolkata.
VMWARE Training in Kolkata.
Citrix Training in Kolkata.
NSX Training in Kolkata.
VDI Training in Kolkata.
You make so many great points here that I read your article a couple of times. Your views are in accordance with my own for the most part. This is great content for your readers.
Best software development Company in dubai
You there, this is really good post here. Thanks for taking the time to post such valuable information. Quality content is what always gets the visitors coming.
Best Digital Marketing Companies in dubai
Wow! Such an amazing and helpful post this is. I really really love it. It's so good and so awesome. I am just amazed. I hope that you continue to do your work like this in the future also
Best Web Development Companies in dubai
I was surfing the Internet for information and came across your blog. I am impressed by the information you have on this blog. It shows how well you understand this subject.
Web Development Agency Dubai UAE
I was reading some of your content on this website and I conceive this internet site is really informative ! Keep on putting up.
Software Development Company in Dubai UAE
This is such a great resource that you are providing and you give it away for free. I love seeing blog that understand the value of providing a quality resource for free.
Mobile App Development Company in Dubai
Positive site, where did u come up with the information on this posting? I'm pleased I discovered it though, ill be checking back soon to find out what additional posts you include.
Software Development Company in Dubai UAE
I wanted to thank you for this great read!! I definitely enjoying every little bit of it I have you bookmarked to check out new stuff you post.
Website Development Company in Dubai
Excellent article. Very interesting to read. I really love to read such a nice article. Thanks! keep rocking.
Ecommerce Development Company in Dubai
Thanks for a very interesting blog. What else may I get that kind of info written in such a perfect approach? I’ve a undertaking that I am simply now operating on, and I have been at the look out for such info.
Best Software Development Company in UAE
I admire this article for the well-researched content and excellent wording. I got so involved in this material that I couldn’t stop reading. I am impressed with your work and skill. Thank you so much.
UI UX Development Company in UAE
A very awesome blog post. We are really grateful for your blog post. You will find a lot of approaches after visiting your post.
Digital Marketing Companies in UAE
At SynergisticIT we offer the best java bootcamp
Thank you very much for sharing useful information. I appreciate your efforts. Get
Top software development services
Best project management services in India
Techsaga Corporations optimally claim our Software development company in Noidaexpertise through the advanced involvement of our Software Product engineering team in devising the most assertive and advanced solutions for you at Tech saga. A software product development team has to perform in accord with the client's engineering team for ideal results.
Software development company in Noida team has to perform in accord with the client's engineering team for ideal results. Techsaga Corporations address this facet of software development process most aggressively with our streamlined and well-planned resources.
Great work. Thank you for being so supportive and solving my issue. I would recommend others to dial QuickBooks Customer Service 877-603-0806 for quick resolution of issues.
Thank you for sharing such a nice piece of content with useful information. Please also visit my site of Luxury Car Rental Dubai. We are offering special discount for everyone. You just need to use code of Online2021
Thanks
Renter Point is a leading platform for all type of rental cars in Dubai, Enjoy great deal on تأجير سيارات دبي from renterpoint.com.
Thanks for the excellent article, I enjoyed reading it! You will find out if you are ready to operate a supermarket. Because suffescom solutions living in a time of high contagion, the task of establishing a trustworthy security system like in-app development and any other services is essential to allowing clients to buy products from your app without being worried about being taken advantage of maximum result.
Nice Post Really Informative
Best App Development Companies
Best Mobile App Development Company
Educational Software Development Companies
Best mobile app development services
Custom application development company
How To Download USB Driver. HP provides the drivers necessary for your printer to work with the operating system on your computer. If your printer did not come with drivers, or you have added another computer to the office, you can get drivers directly from HP. The support section offered on the ..."
The article is very interesting. Thanks for sharing this article. I enjoyed reading your article.
internship for web development | internship in electrical engineering | mini project topics for it 3rd year | online internship with certificate | final year project for cse
Thanks for sharing the informative post. If you are looking the Linksys extender setup guidelines . so, we have a best technical expert for handlings your quires. for more information gets touch with us 토토
바카라사이트 Hi there! This is my first visit to your blog! We are a group of volunteers and starting a new project in a community in the same niche. Your blog provided us useful information to work on. You have done a extraordinary job!
You there, this is really good post here. Thanks for taking the time to post such valuable information. Quality content is what always gets the visitors coming. Feel free to visit my website; 안전놀이터
What a wonderful blog! I read your entire site and found it incredibly useful and easy to follow. A properly fitted leather dog collar should be lightweight and soft, allowing your dog to feel secure without being restricted. A basic collar with ID retention is also advised when additional movement equipment such as a strong dog leashes, harness or backpack is used.
แหล่งรวม เกม PG SLOT พวกเราอาจจะไม่ยอมรับกันมิได้ว่า เว้นแต่เงินรางวัลที่เย้ายวนใจในเว็บไซต์พนัน pgslot แล้ว ภาพ เสียง ในเกมส์เป็นเรื่องสำคัญที่เย้ายวนใจเหล่านัดหมายพนันให้ได้เล่น
สะดวกกับการเล่นที่แปลกใหม่ กับเกมสล็อตค่าย PG มาให้ ทดลองเล่นสล็อต ถอนเงินได้จริง ทำได้ผ่าน สมาร์ทโฟน แท็บเล็ต และคอมพิวเตอร์ได้ตลอด 24 ชั่วโมง เหมาะกับเกมทุกรุ่น ลงเดิมพันการเล่นได้ตามต้องการ เล่นได้ทันทีผ่านเว็บไซต์ PGSLOTGAMES
เกมสล้อตรูปแบบใหม่ ทดลองเล่นสล็อต PG พร้อมทำกำไรได้อย่างหลากหลายรูปแบบ พร้อมเล่นได้เลยทันทีผ่านเครดิตฟรี ที่มีให้คุณได้รับสูงถึง 10,000 เครดิต ให้คุณสามารถนำไปเล่นเกมได้เลยแบบฟรีๆ เปืดประสบการณ์การเล่นอันเร้าใจ พร้อมให้คุณได้เล่นมากถึง 24 ชั่วโมง
คาสิโนออนไลน์ เว็ปพนันออนไลน์อัดับท็อปของประเทศไทย ที่สามารถเข้าทำเงินได้แบบ การันตีความปลอดภัยในการเล่น กับความทันสมัย mega game mega game เว็ปพนันทำเงินออนไลน์ ที่สามารถทำเงินได้แบบไม่มีเงื่อนไข.
ติดต่อเรา pg slot เว็บตรง ลูกค้าสามารถ ติดต่อมาและสอบถาม เนื้อหาการใช้แรงงานได้ทาง PG SLOT หรืออยากได้ อัพเดทข้อมูลโปรโมชั่นใหม่ ลูกค้าก็สามารถแอดไลน์ เพื่อติดตามข้อมูลได้ในทันที
Valuable info. Thanks I discovered this awesome website here.
Lot of informative blog are provided here, Happy to read this good post. Thanks a lot
Type of fantastic informative web site, Awesomeness! Thanks
It’s really a great and useful piece of information. Keep doing it!
I am glad that you shared this useful information with us. Thanks
Everyone loves it Great website, continue the good work! Thanks
I'll bookmark it and come back to read more of your useful info. Thanks for the post.
I am not sure where you are getting your info, but good topic. Just continue updating
I’m glad to read this article. The website style is perfect, I'll support your articles
I appreciating this great blog. Thank you for posting something good like this
Keep us updated, looking forward for more posts you will make. Many thanks
Great job for publishing such a nice article. Its useful for us. Thanks a lot
I checking continuously this weblog and I am impressed! Awesome!
It was definitely informative article you've done. This site is very useful. Thank you
Excellent blog contains a wealth of informative material. I'd adore reading your blog. I'll continue to read your blog.
Amazing. You consistently inspire me. New posts usually have an impact on your blog!
Continue publishing such fascinating articles; your visitors will grow to like your writing.
I absolutely loved every single little bit of it and had a wonderful day.
The information was genuinely fascinating.
I appreciate you sharing such an excellent article.
Nice and undoubtedly excellent blog.
You're really good at creating original material. Working was incredibly enjoyable.
I genuinely like your ideas. Thanks for the excellent information for the plan.
You provided excellent information.
I especially appreciate it please keep doing it.
Post a Comment