This kind of thing is ugly, difficult to manage, and sadly, it's prone to error if not handled properly. However the usability factor for these things, once written properly, can't be argued against.
What makes these things hard is that most frameworks try to prevent you from doing it. For example, Django's form wizard classes are about creating a staged form for a single model, or for some collection of data - not the same model repeatedly.
So, why not just avoid writing these sorts of pages? Why not force users to create one user at a time? Well, there's a few reasons. Sometimes you want to allow the user to enter the information without actually persisting any of it until the very end. Sometimes you want to give the user a method of creating a lot of information at once, because it would be tedious to force them into the entry -> submit -> reload cycle over and over again. There are lots of reasons why these kinds of forms are useful.
So, how can we make it easier to write these kinds of forms? Initially, you may be tempted to submit everything in one huge form, with no field name changes, and do a lot of server side work to group fields back to what they were, which we will now refer to as The Naive Approach. If you do that, you're in for a lot of pain. There are three reasonably sound methods I'm going to discuss, along with the common pitfalls, and how to write these things effectively and in a way that promotes code reuse.
Single-page, submitted via multiple Ajax requests
The downside to this method is that it's flaky - the whole reason for doing these kinds of forms is to try and ensure that everything reaches the server in one piece, so if one fails, they all fail. Doing it this way just moves this problem to another part of the code, so it isn't really recommended. I only offer this is because it's infinitely better than the naive approach of submitting everything in a single, huge form. That, and we're sometimes on a budget and need to add things without disturbing too much code :)
Other than that, form validation in this one quite easy as well - handling each object one at a time means you can handle errors one at a time too, including rendering of validation errors.
Single-page, submitted via a single POST
This next approach is slightly different. Basically, treat each object as a discrete form, on the same page. Divide it however you want, of course; tabs, or something. Basically, treat it like The Naive Approach. The difference is, just before you submit it, recollect those fields and serialise them all to JSON - a list of objects, to be specific. Submit that JSON object however you want, and break it up server side into your discrete objects.
This is nice because it gets around the problems posted by the naive approach, and the previously mentioned approach, using multiple Ajax requests - everything is now sent in one chunk, so you don't have to deal with strange, half-persisted states - it's all or nothing.
The disadvantage to this is the overhead in having to handle re-rendering of validation errors, but it's definitely not impossible.
Multi-page, submitted via multiple POSTs
Finally, the most rigid approach, and most wizard like, is using multiple pages, submitted one at a time. Pages become normal forms this way, and are a lot more manageable. The data can be persisted between pages using a hidden field containing JSON from the previous pages. Alternatively, persist the previous objects server-side in a temporary store, and store the key to this object in a hidden form field. This approach is the most appropriate, as it will prevent the user from modifying the data after you've validated it.
The downside to this approach, is that the user experience is a little dull. You can't dynamically keep adding objects - you need to have two buttons, "Add Another" and "Finished", essentially. Removing objects from this can't be done easily, either - you'd have to have a "final" step, involving an overview where objects can be removed or modified.
I'm leaving the implementation of the above to you, the reader. Any implementation I provided would be barebones and lacking to the point that you'd have to rewrite most of it anyway, which would be a waste of your time and mine. I don't believe the concepts I've discussed are particularly difficult, so, enjoy :)
Further Niceties and Simplifications
There are a few more nice idioms present in the implementations I've provided, which are simply to make the user experience a little easier to manage.
Persist User-entered Data Client-side
Sometimes things go wrong with the forms your users submit, and they close the page. Or a request goes wrong. Lots of bad things can happen. If you persist the information they're entered, using a plugin like typewatch and storing it with HTML5 localStorage means that if it fails, you can bring it back the next time the user visits the page. There's a 5MB limit on this data though, so you don't want to go too crazy with it.
Watch Those Field Names
The Naive Approach is called the naive approach for one reason: field mismatching. If you have a field with the same name displayed multiple times, and the user forgets to fill a field out, then the data is mismatched - there's no way to tell which fields belong together. To explain what I mean in full, imagine you have two user forms filled out, user A and user B. You forget to enter a username for user A, and submit it to the server. On the server, everything is matched together based on position arguments - all the first items in the email, password and username fields are assumed to be user A, all the second belong to user B, and so on. If you forget to fill in a piece of information, suddently user B's username is in position one, and thus attached to user A - not a good place to be. There are lots of dodgy hacks you can use to get around this, like renaming fields to use numbers just before submission, but they should be avoided - go with one of the approaches I've outlined above :)
I think for more modern, interactive applications, the single page approaches outlined above are quite suitable, as they allow for much more flexible validation and modification of previous data. I believe submitting via a single POST is the most robust, and I can't think of a reason you'd use it over multiple POSTs.