Cheat Sheet: Architecture and Design Best Practices at a Glance
- J.D. Meier, Alex Homer, David Hill, Jason Taylor, Prashant Bansode, Lonnie Wall, Rob Boucher Jr, Akshay Bogawat
This cheat sheet presents a wide-ranging overview of the key factors involved in designing the architecture for your applications. It describes the general steps in the process, the types of application you can build, the typical layered architecture for your applications, and the design approaches for data access, services, and quality attributes.
- Learn about the typical general approach to designing application architectures.
- Learn about the different types of applications that you can build.
- Learn about the key factors you must consider when designing application architectures.
- Learn about the layered approach to application design.
- Learn about the quality attributes that affect the overall success and acceptance of an application.
The following guidelines will help you to understand the typical features of the design and architecture process.
- How to design your architecture
First determine the objectives of your architectural design, which will help you, focus on solving the right problems. Then identify the key scenarios or use cases that are most critical from your application perspective and have maximum impact on the architecture. The use cases will help in focusing on the right problems and also help in evaluating and reviewing the architecture. To achieve the architectural objectives, identify the application type, deployment architecture, architecture style, and related technologies in view of the identified key scenarios. While doing so ensure that you take into account the infrastructural and organizational constraints. Identify the quality attributes such as performance, security, etc and cross-cutting concerns like exception handling, auditing and logging, etc that are important from application perspective, to make sure that the architecture adheres to the requirements and objectives for them. Finally define the architecture solution and review / validate against the key scenarios identified.
- How to choose a deployment topology
When you design your application architecture, you must take into account corporate policies and procedures together with the infrastructure that you plan to deploy your application on. If the target environment is rigid, your application design must reflect the restrictions that exist in that rigid environment. Your application design must also take into account QoS attributes such as security and maintainability. Sometimes you must make design tradeoffs because of protocol restrictions, and network topologies. Identify the requirements and constraints that exist between application architecture and infrastructure architecture early in the development process. This helps you choose appropriate architectures and helps you resolve conflicts between application and infrastructure architecture early in the process. Use a layered design that includes presentation, business, and data access logic. A well-layered design generally makes it easier to scale your application and improves maintainability. A well-layered design also creates predictable points in your application where it makes sense (or not) to make remote calls. To avoid remote calls and additional network latency, stay in the same process where possible and adopt a non-distributed architecture, where layers are located inside your Web application process on the Web server. If you do need a distributed architecture, consider the implications of remote communication when you design your interfaces. For example, you might need a distributed architecture because security policy prevents you from running business logic on your Web server, or you might need a distributed architecture because you need to share business logic with other applications, Try to reduce round trips and the amount of traffic that you send over the network.
- How to structure your application
Start at the highest level of abstraction and begin by grouping your application functionality into logical layers. Understand the available deployment strategies for the physical structure and tiers, and identify how the logical layers will map to the physical tiers. Identify communication strategy and the protocols that can be used for communication between the layers and tiers in your application. Define interfaces for the layers, the information that will be communicated between layers, and the form of that communication.
- How to decide on your layering strategy
When grouping the components in a layer, make sure that the components depend only on components in the same layer or components in a lower layer. Use loose coupling between layers. Avoid any circular dependencies between layers. Use only the layers that are required, and are relevant to your scenario. For example, if your application does not have a user interface, you will not require a presentation layer. If your application has minimal or no business logic (for example, a reporting application), you may not require a business layer. If your business logic is not shared by other applications, or is not located on remote tier, you may not require a service layer. If your application does not use an external data source, you may not require a data access layer.
- How to perform architecture and design reviews.
Identify the architecture objectives and key scenarios for your applications that have most impact on the architecture of the application. Establish design and architecture standards for your application; use these standards to perform design and architecture inspections, using question driven approach with respect to architecture objectives, infrastructure and organizational constraints, performance, and security goals, to focus your efforts. Use the key scenarios identified for scenario-base evaluations, such as Software Architecture Analysis Method (SAAM), Architecture Tradeoff Analysis Method (ATAM) etc.
The following guidelines will help you to understand the fundamentals of choosing an application type, and learn the capabilities and design fundamentals of each type.
- How to choose an application type
If the majority of your users have handheld devices, your application must support offline or occasionally connected scenarios, and does not require a complex User Interface (UI), consider building a mobile application. If your users work with standard PCs and require a highly interactive and responsive application, and your application must use the resources of the client computer and\support offline or occasionally connected scenarios, consider building a rich client application. If you want to deploy your application over the Internet, support a rich interactive and responsive UI, and use client-side processing, consider building a Rich Internet Application (RIA). If your application must expose functionality to clients or be consumed by other applications over the Internet or intranet, consider building a service application. If you do not require a rich interactive UI, but do want to deploy over the Internet and maximize platform and browser independence, consider building a Web application.
- How to design a mobile application
Design specifically for the device instead of trying to reuse the architecture or UI from a desktop application or a Web application. Consider screen size and format, CPU performance characteristics, memory and storage space, development tool and environment support, as well as user requirements and organizational constraints, when choosing which device types to support. Design your caching, state management, and data access mechanisms with intermittent network connectivity in mind.
- How to design a rich client application
For reusability and testability, separate the presentation logic from the user interface implementation using a design patterns such as Model-View-Controller (MVC). Design to provide a suitable and usable interface in terms of layout, navigation, choice of controls, and localization. Extract business rules and other tasks not related to the user interface into a separate business layer. Use a message-based interface to communicate with services deployed on separate physical tiers. Avoid tight coupling to objects in other layers by using common interface definitions, abstract base classes, or message-based communication. For example, implementing the Dependency Injection and Inversion of Control patterns can provide a shared abstraction between layers.
- How to design a Rich Internet Application (RIA)
Plan to use a Web-based infrastructure, because RIA implementations require a similar infrastructure to Web applications. Design your application to run in the browser sandbox. When designing a RIA, consider using a single page that changes dynamically as the user works with the application. Multipage designs are more complex in a RIA, and require additional considerations such as deep linking and UI screen navigation. Provide for usability features such as the ability to pause and navigate to the appropriate point in a workflow without restarting the whole process. RIA implementations have a small footprint on the client, but require a browser plug-in. Design for scenarios in which the browser plug-in is not already installed, including non-interruptive plug-in installation and displaying informative error messages if an installation problem should occur.
When designing a service there are general guidelines that should be followed. For example; design for extensibility, use coarse grained interfaces, never assume how the client will use the service, and decouple the interface from the implementation. The best way to support these guidelines is to introduce a service layer in your design that sits between consumers of the service and the business layer that supports the service. Within the service layer you define interface contracts, classes to translate between the interface and the business layer, and concrete classes that implement the interface. In most cases, interaction with the business layer is accomplished by using a Façade pattern, which allows you to combine multiple business operations into a single application scoped service operation.
- How to design a Web application
The following guidelines will help you to understand the fundamental cross cutting factors when designing your chosen application type.
- How to design your exception management strategy
Use structured exception handling to build robust code. Use exceptions instead of error codes where possible. Do not reveal internal system or application details, such as stack traces, SQL statement fragments, and so on. Ensure that this type of information is not allowed to propagate to the end user, or beyond your current trust boundary. Fail securely in the event of an exception, and make sure your application denies access and is not left in an insecure state. Use finally blocks to guarantee that resources are cleaned up when exceptions occur. For example, close your database connections in a finally block. Do not log sensitive or private data such as passwords, which could be compromised. When the exception contains user input in the exception message, ensure that you sanitize and validate it. For example, if you return an HTML error message, you should encode the output to avoid script injection.
- How to instrument your application
Instrumentation is used when you have a specific problem and you need additional information in order to solve that problem. This could be a debugging issue, a performance issue, a security issue, a manageability issue, etc. This is different than logging in that logging is a general approach to pushing information into log files that may need to be audited in the future vs. a targeted approach to get information for a specific problem. Options for instrumentation include: event tracing for Windows, trace and debug classes, custom performance counters, and Windows Management Instrumentation (WMI). For logging, consider the Logging Application Block in Enterprise Library.
- How to design for transactions
Use connection-based transactions when accessing a single data source. Where you cannot use transactions, implement compensating methods to revert the data store to its previous state. Avoid holding locks for long periods; for example, when using long-running atomic transactions. Consider using compensating locks for long-running transactions. If the chance of a data conflict from concurrent users is low (for example, when users are generally adding data or editing different rows), consider using optimistic locking during data access. If the chance of a data conflict from concurrent users is high (for example, when users are likely to be editing the same rows), consider using pessimistic locking during data access. If transactions take a long time to complete, consider using asynchronous transactions that call back to the client when complete. You can perform transactions using T-SQL commands, ADO.NET, or System.Transaction. T-SQL transactions are most efficient for server-controlled transactions on a single data store. For most transactions in .NET the recommended approach is to use the implicit model provided by TransactionScope in the System.Transactions namespace. Although implicit transactions are not as quick as manual, or explicit, transactions, they are easier to develop and lead to middle tier solutions that are flexible and easy to maintain. If you do not want to use the implicit model for transactions you can implement manual transactions using ADO.NET or the Transaction class in System.Transactions. Regardless of your choice of transaction type, keep transactions as short as possible, consider your isolation level, and keep read operations to a minimum inside a transaction.
The following guidelines will help you to understand the fundamental factors when designing the presentation layer for your application.
Assume that all input data is malicious. Constrain, reject, and sanitize your input; it is easier to validate data for known valid types, patterns, and ranges than it is to validate data by looking for known bad characters. Validate data for type, length, format, and range. Consider using regular expressions for validating the inputs. For user experience consider using client-side validation, but always perform validation at the server as well. Encode your output.
- How to use the MVC pattern
To use the Model View Controller (MVC) pattern effectively, you must understand the division of labor within the MVC triad (the Model, the View, and the Controller). You must also understand how the three parts of the triad communicate with each other to process requests from user input. The Model represents data and the View is the user interface, which displays the data to the user and contains controls for the user to interact with the data and the application. The Controller is responsible for handling requests, initializing the Model and choosing the appropriate View. Common implementations use HTTP handlers or ISAPI filters to intercept requests and send them directly to a controller. There are two main variations of the MVC pattern: the Passive Model and the Active Model. In the Passive Model pattern, changes to the Model are only captured when the Controller processes a request. However, in the Active Model pattern, an Observer pattern implementation can be used to notify the View and/or the Controller when changes occur in the Model. One of the limitations with this pattern is that, because the controller is responsible for intercepting and handling requests, you lose capabilities associated with the View such as the ability to handle control events and the use of viewstate in ASP.NET.
- How to use the MVP pattern
The Model View Presenter (MVP) pattern is very similar to the MVC pattern with the main difference being that views handle requests and pass them to a presenter, which provides controller logic. Similar to the controller in MVC, the presenter is responsible for processing requests and initializing the model. The main advantage of using this pattern over MVC is that because the view handles requests you also have support for control events and the ability to maintain the state of controls in the view. There are two main variations on this pattern; Passive View and Supervising Controller. With Passive View requests are intercepted by the view, passed to the presenter, and then the presenter initializes fields in the view through an interface. This variation completely decouples the view from the model and provides the highest level of testability. With Supervising Controller the view passes requests to the presenter, the presenter notifies the view when the model is initialized, and the view accesses the model directly. This variation is useful when you have a lot of data that needs to be presented in the view, which makes passive view impractical.
The following guidelines will help you to understand the fundamental factors when designing the business layer for your application. The main factor that you should consider is how you will implement business entities.
- How to implement business entities
If you are designing a small Web application or a service, and you want to take advantage of the disconnected behavior they provide, consider using DataSets. If you are working with content-based applications that have few or no business rules, consider using XML. If you have complex business rules related to the business domain, or if you are designing a rich client where the domain model can be initialized and held in memory, consider using custom Domain Model objects. If your tables or views in the database represent the business entities used by your application, consider using custom objects. If the data you are consuming is already in XML format, or if you are working with read-only document-based data, consider using custom XML objects.
The following guidelines will help you to understand the fundamental factors when designing the data layer for your application.
- How to design your data access layer
If you choose to access tables directly from your application without an intermediate data access layer, you may improve the performance of your application at the expense of maintainability. The data access logic layer provides a level of abstraction from the underlying data store. A well-designed data access layer exposes data and functionality based on how the data is used and abstracts the underlying data store complexity. Do not arbitrarily map objects to tables and columns, and avoid deep object hierarchies. For example, if you want to display a subset of data, and your design retrieves an entire object graph instead of the necessary portions, there is unnecessary object creation overhead. Evaluate the data you need and how you want to use the data against your underlying data store.
- How to design your connection management approach
Pool connections. Connections are an expensive and scarce resource, which should be shared between callers by using connection pooling. Opening a connection for each caller limits scalability. Connect by using service accounts. Open database connections right when you need them. Close the database connections as soon as you are finished. Do not open them early, and do not hold them open across calls.
- How to choose between stored procedures vs. dynamic SQL
When discussing the choice between stored procedures and dynamic SQL the focus is on SQL dynamically generated in code versus SQL that is implemented within a stored procedure. When choosing between stored procedure and dynamic SQL you need to consider the abstraction requirements, maintainability, and the environment constraints. If you have a small application with limited business rules then dynamic SQL is often the best choice. On the other hand, most applications benefit from the use of abstraction, which means you’ll need to choose where that abstraction should exist; at the database in the form of stored procedures, or in the data layer of your application in the form of data access patterns or object/relational mapping (O/RM) products. With stored procedures most changes to the database schema will have a minimal impact on application code. However, the use of stored procedures also requires resources that are familiar with database code. In addition, stored procedures are harder to debug. When implementing your abstraction in the data layer you are essentially using dynamic SQL, which is also easier to debug. In addition, there are libraries and products available that make it easier for non-database experts to create statements that interact with the database. The downside to dynamic SQL is that changes to the database schema will have an impact on application code, which will require rebuilding and deploying updates. For security considerations you should always use typed parameters, either passed to a stored procedure or used when creating dynamic SQL. Finally, the one practice you should always avoid is mixing business rules with the generation of dynamic SQL.
- How to improve data access performance
Minimize processing on the server and at the client. Minimize the amount of data passed over the network. Use database connection pooling to share connections across requests. Keep transactions as short as possible to minimize lock durations and to improve concurrency. However, do not make transactions so short that access to the database becomes too chatty.
- How to pass data across layers and tiers
Consider scalar values when the consumer is interested only in the data and not the type or structure of the entity. Do not consider scalar values if your design is not capable of handing schema changes. Consider XML strings when you must support a variety of callers, including third-party clients. Consider custom objects such as Data Table Objects (DTOs) when you must handle complex data, communicate with components that know about the object type, or require better performance through binary serialization. With the .NET Framework, do not pass DataReader objects between layers because they require an open connection. DataSets, however, provide great flexibility and can be used to cache data across requests. Do not use DataSets when they are more expensive to create and serialize compared to the performance gain achieved through caching.
The following guidelines will help you to understand the fundamental factors when designing the service layer for your application.
When designing services, you must consider the availability and stability of the service, and ensure that it is configurable and can be aggregated so that it can accommodate changes to the business requirements. In most cases you want to design services that are autonomous, provide explicit boundaries, do not expose internal classes, and use policy to define interaction requirements. You should also: Design for idempotency so that the service can manage messages that arrive more than once. Design for commutativity so that the service can handle messages that arrive in the wrong order. Design for invalid requests by validating them against a schema or known format. Consider using standard elements to compose the complex types used by your service.
- How to expose your application as a service
The approach you take when it comes to exposing an application as a service depends on where you are in the development lifecycle of the application. If you already have an application and wish to expose operations as services you should identify the necessary operations and then define interface contracts that combine operations to produce application scoped procedures. The main thing you do not want to do is expose component or object based operations as service operations. When starting from scratch you should first define the service interface contracts that an application needs to support. The main goal is to provide coarse grained interface contracts that support business process or client requirements without focusing on the implementation. Once you have the contracts defined you can then focus on how to implement code that supports the contracts.
- How to choose between ASP.NET Web Services and WCF Services for Services
ASP.NET Web Services are a good choice for simple HTTP-based services hosted in IIS. WCF is a good choice for building services with a SOA-based approach. WCF is also a good choice if you need the performance of TCP communication over HTTP or if you need to host the service without a Web server. WCF provides support for WS*, which includes support for end-to-end security and reliable communication. WCF allows you to implement duplex communication, and you can also use it with Windows Message Queuing and as a Windows service. In addition, you have more options with regard to protocols, bindings, and formats. Keep in mind that WCF requires .NET 3.0 or higher.
- How to choose REST vs. SOAP
Use Representational State Transfer (REST) when you want to use the Web as an open publishing medium, require stateless and/or synchronous interaction, must support resource-oriented user interaction, and require proven scalability. Use SOAP when you need a cross-enterprise communication medium that allows asynchronous activity-oriented or service-oriented interactions, provides a standardized approach to accessing resources, and supports orchestrated event flows.
The following guidelines will help you to understand the fundamentals of applying quality attributes to your application.
- How to design for security
Use threat modeling to systematically identify threats instead of applying security in a haphazard manner. Itemize your application’s important characteristics, assets, and actors to help you identify threats. A detailed understanding of your application will also help you uncover more relevant and detailed threats. Use a security frame to focus on areas where mistakes are most often made. Key categories in a security frame include: auditing and logging, authentication, authorization, configuration management, cryptography, exception management, input and data validation, and sensitive data. Rate the threats based on the risk of an attack or occurrence of a security compromise and the potential damage that could result. This allows you to tackle threats in the appropriate order.
- How to design for performance
Use performance modeling early in the design process to help you evaluate your design decisions against your objectives before you commit time and resources. Identify your performance objectives, your workload, and your budgets. For example, performance objectives may include maximum execution time and resource utilization such as CPU, memory, disk I/O, and network I/O. Identify your constraints, such as time and hardware budget. Use load testing and unit tests to measure performance, and identify if hardware or deployment scenarios are the cause of bottlenecks. Ensure that you test with data types and data volumes that match the actual runtime scenarios.
- How to identify and evaluate performance issues
Focus on the critical areas where the correct approach is essential and where mistakes are often made. Identify requirements, cost, and budget restraints; and whether improvements can come from additional hardware and infrastructure, improved application code, or by adopting a different deployment approach. Perform design inspections and code inspections to identify poor practice that could lead to bottlenecks. Organize and prioritize your performance issues using a performance frame. Key categories in a performance frame include: data structures and algorithms, communication, concurrency, resource management, coupling and cohesion, caching, and state management.