Goal: Conceptual Integrity
First and foremost, we want our packages to mean something, to make a Statement. The Statement should be simple and bold, and everything within the package should be geared toward that goal. Statements like "THIS is how the user interface works!" or "Interact with networks here!" are good. Statements like "talk to Databases and interact with users" are probably too complex, but we might be able to rephrase with "Let users manage databases" and be okay. Conceptual Integrity allows people looking for solutions to know if this package is right for them, and it allows everyone working on our application to know if they're developing for this package. It allows us to partition requirements into logical pieces, such that one requirement change will impact the smallest number of packages, and thus hopefully the smallest amount of code. It's probably the single most important reason to have multiple packages.
Goal: Reuse
Just like we want methods reused and classes reused, we want our packages to be reused when appropriate. We want to take that package we wrote for feature 1 and use it for feature 2. Probably this means some refactoring – the first pass may have been pretty feature specific. It's worth the effort, though:
We would love for that package to be useful in a completely different application. If we can get someone else using it, any defect they find is one we don't have to look for. Fix it, and make the package better for everyone. Don't waste time trying to write the ultimate reusable package, though. We're supposed to be writing an application, not a library.
Good Practice: Cautious Outer-App Package Dependencies
Maintenance is not just about requirements: we also have to worry about packages we depend on. Every time a package dependency changes, we need to at least run through an entire testing suite. At worst, we need to do a very big rewrite. This means we want to rely on packages that aren't going to change much. Note this doesn't necessarily apply to packages within our developing application; this means all those 3rd party packages we're trying to exploit. Make sure they're solid.
Good Practice: Avoid Dependency Cycles
Dependency cycle: package A depends on package B which depends on package C, which depends on package A. Avoid them. For one thing, as my college Brian points out, if we have dependency cycles, we're probably violating conceptual integrity in some of those packages, and so we're losing the biggest use for our packages.
Another reason cycles are bad: possible infinite testing. If we have control over all the packages involved, we can cheat here. Sadly, we don't always have that luxury. Here's the problem: Package A is updated, and package C needs to change in response. Now package B has to be checked and also gets updated. Suddenly we're back to package A – dependency cycle. Without exclusive control over everything involved, this could go on ad nauseum. The whole set, packages A, B, and C, might as well be collapsed into one big package while such a lock exists, and that huge package will not have a simple Statement. We've just broken Conceptual Integrity. Cry.
Good Practice: Package Ownership
For each package, define one person to be the "Owner". The primary responsibility is to ensure the package maintains its conceptual integrity: that it remains true to its cause. This person waves the red flag if something goes wrong with the package; this person watches for dependency cycles; this person probably writes lots of tests for the package. This person is the package's mother.
Question: Where's the glue?
"Reuse is great and all, but we can't take Bob's Database Package and use it out of the box – we have to write special code to make it do what we want to do." It's this glue code that makes an application something useful to Housewife Sally, who doesn't know or care about code.
Whoa, slow down big fella. Our packages ARE the glue code. We're not writing libraries, remember? We're writing an app. It's okay to have a package where various pieces come together; somewhere in all this, after all, we need to RUN the program. That can be a package Statement: "This package makes the program go."
Question: Dependency paths and fixing bugs
So, we've got this great package, but we found a bug. And we fix the bug. Good for us! We're even pretty sure we fixed the bug without any repercussions; our whole super testing suite is solid and passes everything just fine. This all means that packages that depend on us don't have to worry about anything, right?
Wrong. For one thing, we changed code. "That means we may have injected a defect", says someone with super clean hands. Small chance, maybe, but dependant packages can't take the risk – they're at least going to run through their test suite.
For another thing, we may have changed something circumstantial that outside packages depend on. Maybe we used to return a list sorted by last name, and now we return it sorted by first. Doesn't matter to us, all we promised was a list, but somewhere someone realized they were all sorted one way, and used that fact. Now that assumption (bad on their part) is broken, and they have to deal with it. Not our fault, per se, something they have to take into account.
Summary
Developing to packages within our application is harder than developing just to our application. Packages are intended to be reusable, which means more flexible. More flexibility means more paths of use, and in turn means more possibility of defects. This means more initial design, more development, and more testing. Why program to packages, then? All the standard, good reasons: Abstraction, Encapsulation, Information hiding, Maintainability, Understanding. Working hard to ensure we have a well architected system will save us down the road. But we need keep a good head on our shoulders - it's easy to try to make things too flexible, too general when our app doesn't need it. And the app is the reason we're here at all.