10 Steps To Build A Successful MVP

Sean Cannon
Presence Press
Published in
10 min readNov 25, 2017

--

Congratulations! Our agency has been commissioned to build and deliver a MVP for a respectable client. What next?

Before we begin, I want to solidify some nomenclature. I propose “MVP” differs from “prototype”. Prototypes are typically investor-facing or board-facing. Something to prove a concept for securing funding and/or budget for the real thing. I propose MVP is customer-facing, and may even come directly from prototype approval. It’s a brand new thing that the client believes will gain traction and grow. Prototypes do not grow. We build them as fast and as affordable as we can to prove that the ask is technically possible so the client can make some decisions. Once that is proven, we have options on how to turn it into a product, but none of those options should be extending the prototype code. That should be used as reference only. I’ll use a web app as an example for this article, but the concepts translate across industries regardless.

I also want to make a point to mention that I try really hard to stay out of the weeds in this list. I reference some coding patterns and bash commands, and provide some screen grabs from IntelliJ here and there, and I’m happy to offer further explanation in a comment, but ultimately I don’t want to define the syntax for how you accomplish this goal — just the process.

Let’s start with the [not-so] common sense check list:

1. Define Success

Before we write any user stories or code anything, we need to understand what defines success for this project. In other words, what measurable acceptance criteria will prove the app is doing what it needs to do and that our agency delivered what was asked? Never, ever, begin development until client-subjective becomes agency-objective.

2. Define The MV

  • Minimum : We scope ONLY the features required to deliver the MVP. Zero nice-to-have features will be built for 1.0.0. If it’s not in the acceptance criteria from the success definition, it’s a nice-to-have feature.
  • Viable : We scope ALL the features required to deliver the MVP. Zero must-have features will be omitted. All must-have features should appear in the acceptance criteria from the success definition.

3. Define Services

Here is where we start making epics. Some examples:

  • E-mail
  • SMS
  • Authentication
  • Static Assets
  • Search
  • Cache
  • Databases
  • Message Queue
  • Social Networking
  • Analytics
  • Logging

Note : We will want our list to line up nicely with our M and V. For example, if our client is a bank, and the SMS is intended for two-factor auth, it’s a must-have. If it is intended for happy-birthday messages, it is a nice-to-have.

Notice at this point we don’t have any epics for the application logic. Just the service layer and the utility layer. Our app’s IP will leverage these services agnostically. The client needs to be able to swap out any provider of any of the above services or use various providers on a given condition within reasonable expectations. The core IP shouldn’t know or care when that happens.

4. Define The Stack

At this point we should understand what the MVP should do and who the target audience is. We want to ensure the stack can support the following :

  • Request load from users
  • Communication with all the external services (REST, SOAP, etc)
  • Most common (highest-revenue-potential) user-agent
  • Internally-managed services (infrastructure not dependent on a 3rd party API)

My rule of thumb is to create the app in a way that it doesn’t know if it’s being run on my Mac or in a node cluster in AWS. Using Docker for the app is sometimes a bit cumbersome but it forces us to deal with dependency I/O in an elegant way from the start. Scaling and environment control is a DevOps task — our job at this level is to sandbox our app so it makes no assumptions and makes DevOps easy. Use environment variables when at all possible, allow for easy key rotation, etc.

5. Define The Persistence Layers

Is our MVP relational or not? Do we need strict schemas or flexible models? Answer these questions and we’ll filter our database and cache options. I typically recommend leaning towards whatever database we’re most comfortable with and which lets us scale appropriately. 90%+ of our projects work beautifully in front of Redis/MySQL. AWS has flavors of these which scale just fine, and both of these are very easy to mock out and setup locally. MySQL 5.7 offers JSON support so if most of our app is relational but we still want to support some schema-less entities, we can. Whatever flavor of cache/db you choose is up to you. Just don’t back yourself into a corner.

Slow MVPs are bad news. All our reads should come from Redis when possible, regardless of indexes on the db. I put hooks in my controllers to sync up with Redis on every level of CRUD so the cache gets invalidated and updated in real time. Redis is actually extremely powerful for querying and filtering, and makes dealing with the API layer concise, intuitive, and simple.

6. Start Coding

I highly recommend test-driven, functional programming. 100% code coverage is just a starting point. We absolutely can not afford to spend time fixing bugs for an MVP. Be disciplined and write tests. Clients often think that skipping tests will save them money, but in reality it costs them much more. Debugging always takes more time than writing tests, and dealing with customer-facing bugs often requires patch-code to be written with a hasty deployment, as well as some inconvenience-apology to be written and sent to the customers. That’s never cool.

  • Write the initial database migration SQL with all the tables, constraints, etc. I typically put placeholder strings in the file so I can regex-replace them with environment variables at demo time/test time and have a unique database per environment if necessary. This makes it really easy to unit test as well as have a demo database we can use for consistent presentations to the client. Consider the following — migrationSql.replace(/__CORE_DB_NAME__/g, process.env.CORE_DB_NAME) — against this SQL (below), where CORE_DB_NAME could equal ‘core-test-client-mvp’ or ‘core-demo-client-mvp’:
  • Write some mock data to be inserted into each table during the unit tests. I prefer CSVs, but choose what works best for you. Every test tears down the database, re-runs the migration SQL (pre-cached), and seeds (LOAD DATA LOCAL INFILE). Every test is 100% pure and has no side effects. No tests depend on any other tests. Writing these CSVs also produces early indications of any mistakes in foreign key mappings and we can get those ironed out. All these initial CSVs should live in a test folder. That folder conveniently matches the NODE_ENV variable:

As seen above, we should also copy those into a demo folder and populate the files with some more demo-friendly values.

  • Make the CRUD. Models and controllers for each table in the database. Unit tests for each method (controller tests and model tests). Unit tests should test each validation rule, and each validation rule should line up with the database column definition. For example, a column of VARCHAR(30) NOT NULL should have validation ensuring a string of length between 1 and 30, and we should have tests proving all that. Make both the controllers and the models pure. For example, a controller that accepts and mutates the req object in Express is not pure.
  • Once the CRUD is done and tested, we need to write a demo script which launches our app with NODE_ENV=demo and runs the same script that loads CSVs into our database. Prove that it all works with Postman or another XHR tool.

7. Define The First Strategy For Each Service

We should not assume a service provider is permanent enough to be referenced in our application logic, ever. They change all the time.

Take Email for example. We have options: Mandrill/MailChimp, Sendgrid, etc. The MVP probably defines that users can receive emails for sign-up verification, password-reminders, etc. What tool is used is likely just a budget decision — they all live in the cloud these days and scale pricing.

Our application code should not care about any of that. It should only know about Email. How do we accomplish this? We define an interface with methods that every Email provider would reasonably satisfy in their own API or SDK. Let’s assume the client chose Mandrill — the docs might have 20+ functions we can call, but right now our MVP only needs send so let’s start there.

Continue this process for each of the services required in the success definition. It should start to look something like this:

Of course it goes without saying that as we’re coding up these files, we’re also coding up tests.

8. Develop The IP

All this time we’ve been coding, the product manager has been working with the client and they’ve been changing their mind on things. Every project ever, this happens. I hope, though, that you can see what we did while that was happening. Changes at this point now are extremely easy. They don’t ruin our app, and they don’t create technical debt. They’re isolated and can actually be scoped. We can actually answer the question “How long will it take if we want to switch to Sendgrid? The Mandrill contract was more than we thought it would be?” — in all actuality how long does it take to copy the Mandrill strategy folder, look at the Sendgrid docs, swap in the API calls, and copy over some tests with a new mock? Not long. Bonus, they can keep the Mandrill strategy because deleting it serves no benefit.

Another benefit of having setup our services the way we did is that the services and their tests know nothing about the client or the MVP being developed. Pending contractual permission, we can leverage any or all of these services for the next project and have options on a more competitive price or a better ROI.

Now that the services and CRUD are complete, we can build the application logic. This is the business logic that makes our MVP unique compared to other apps. Lots of apps send emails, but only ours can _____. We already have controllers and models setup nicely with established patterns and cache syncing. Let’s copy the patterns already in place from the CRUD, leverage the services we developed and now, finally, we can write the code the client cares about. We will work directly with the client at this stage to make sure the app is doing exactly what the client wants.

If the IP primarily lives on the server, great — we haven’t built any UI yet so let’s keep going with the JSON. Our unit tests are proving everything is wired up and Postman is proving that our app works in the application framework like ExpressJS.

If the IP primarily lives on the front-end, we can start building this out. I recommend starting a brand new repo for the front-end at this point. It makes scaling easier and DevOps will appreciate the abstraction. It’s also fantastically easy to have two apps talk to each other on a local machine. Docker compose, a shell script, or just open them both up in your IDE and yarn run demo on each. Lastly, it makes it such that when we decide to support the next client type (kiosk, mobile app, electron app, .NET app, etc) we won’t have to worry about dealing with the API server being baked into the first client.

9. Deploy

Awesome. The MVP does exactly what it needs to do, and all that time spent setting up our services and writing the tests made it such that the business logic was a breeze and the client feels free to grow and adapt as the market demands. DevOps can take over from here.

We have patterns and tests in place now that allow for new services and CRUD to be implemented intuitively and safely in the future. The client is able to leverage a more novice and affordable team to take over who can copy those patterns outright.

Complete test coverage allows the client to perform rapid A/B testing and implement throwaway marketing campaigns against the app without worrying about breaking the UX or worse.

We also have a collection of patterns and scripts that we can use again and again and again. These scripts are separate from the intellectual property, and are merely boilerplate which allow us to write modular software. Nobody can claim ownership of patterns or open source code libraries, and so establishing these is what will make us a profitable, competitive and competent agency.

10. Celebrate

--

--