Avoid the waterfall trap
It’s ok to start development of your app before the architecture is complete. The temptation (myself included) is to build out the full architecture in one large waterfall effort. The problem with this approach is that you’ll have one or more early sprints where no value is delivered to the stakeholders. Additionally there may be an unforeseen change that could render your master plan worthless. So, pull in just a few architecture chores each sprint. Implement your features/user stories such that they can be easily refactored to use future architecture. For example – you may not have time to implement an IOC container, data access layer, and business-value features in the first sprint. That’s ok – skip the IOC and implement it later. You’ll have a few classes that might look like this:
public class QuoteController
{
private readonly IQuoteService quoteService;
public QuoteController()
{
this.quoteService = new quoteService();
}
}
We’re manually instantiating a concrete implementation of IQuoteProvider in the example above. By no means is this optimal, but it works. Sprint two rolls around and you implement an IOC container and set up some constructor DI. Your few existing classes only need some light refactoring. Your QuoteController would look like this:
public class QuoteController
{
private readonly IQuoteService quoteService;
public QuoteController(IQuoteService quoteService)
{
this.quoteService = quoteService;
}
}
You were able to deliver something with business value in the first sprint, and the end-result from an architectural standpoint was exactly what you wanted. Everyone wins and everyone is happy.
Core components should be swappable
A good architecture should be able to handle change – fairly drastic change – even during later sprints when the app is mature. The decisions you make early on are critical to ending up with such an architecture. How do you accomplish this? Design your app such that core components like ORMs, loggers, IOC/DI containers, etc can be swapped with relative ease. For example –
avoid directly referencing library-specific types like an EntityFramework DbSet, nHibernate Session object, or Dapper DbConnection. Instead create a domain context (IDomainContext) that lets your app work with the data model in an ORM-agnostic way by providing IQueryables and generic add, delete, and transactional functionality. Another example: for IOC/DI use a generic dependency resolver rather than directly referencing a specific container to resolve objects. MVC and WebApi have a great built-in resolver.