Many times I would consider an assembly boundary to be the boundary between the tiers (presentation, business, and data) of an application as well, but there are just as many cases where I would break the processing tasks handled by a specific tier into two assemblies in order to accommodate a technical requirement.
One component per assembly or project
Unless the component is incredibly complex—which isn’t a good thing anyway—this level of granularity makes development projects difficult to manage. Developers working on even a moderately sized project would find it difficult to code between all of the component files. Moreover, it becomes a deployment and support nightmare since you must deal with versioning issues caused as new versions are introduced into the production environment. Visual Basic developers have grown up working this way since versions of VB that came out before .NET only supported a single class per file. But now that developers using any .NET language can manage multiple classes per file, it makes more sense to group related classes together. The one component per assembly approach also makes the application more resource-intensive at runtime. More components mean longer load times, and more memory and processing overhead to manage them.
One assembly for each tier in an application
For many small to medium-size projects, it makes sense to put all of the components related to an application tier into the same assembly. The logical separation between presentation layer, the business logic layer, and the data access layer is basically an architectural way to minimise the amount of additional work necessary to add features to any layer that can be consumed by another layer. It would seem to make sense to physically separate the components into assemblies that represent each tier of the application. While this sounds good from a theoretical perspective, there are operational reasons against it.
You should consider locating the presentation layer and business logic or business logic and data access layers in the same assembly where it’s required, to ease deployment and systems management concerns. For example, this distribution of functionality will almost always be a requirement in tightly coupled, highly transactional environments where the decrease in an applications’ execution speed created by separating the business logic from the database logic would keep the application from meeting the business need for efficient transaction processing. By deploying separate assemblies for the business and database tiers, you incur the additional overhead of calling cross process and (in a physical three-tier deployment) cross machine, both of which take a considerable toll on the overall speed of the system. Placing two of the three tiers in the same assembly and on the same machine eliminates both of these bottlenecks.
Assemblies by function rather than by tier
In more complex applications, it sometimes makes sense not to create assemblies based on tiers, but instead to create the separation based on function. For example, you should consider grouping business logic and its associated data access logic for a subsystem of a larger solution in a single shared enterprise services package. This way, you run the presentation logic or a business facade on one machine and then consume this subsystem from another machine. Breaking up functions this way also allows the subsystems to be reused in multiple applications more easily than sharing just a data access layer.
When designing your application, take the time to consider how you’ll design assemblies to match your deployment objectives. I’ve seen many cases where the architect makes wise design decisions on how to compartmentalize the functionality of the components in a system, only to have the application fail because of a lack of proper deployment planning.