Here are two options you could consider; an easy one and one that you'd find in commercial games.
Solution 1: Separate your movement axes
If your objects are all rectangles (assumed to be axis-aligned), you could consider horizontal and vertical motion separately. This means your motion procedure will be updated to:
// Horizontal movement
x_previous = x;
x += velocity_x;
if (colliding()) { x = x_previous; }
// Vertical movement
y_previous = y;
y += velocity_y;
if (colliding()) { y = y_previous; }
Note that this will only work assuming all-rectangular geometry and small movement steps. For example, your objects would not be able to through an upward corridor smoothly as they will - as a matter of speaking - alternate between banging into the ground and into the roof.
An additional disadvantage of your technique is that objects will not be able to stop right against the wall. For example, if their horizontal velocity is 10 and their is a wall nearby, they will end up with a gap of 0 to 9 units between them and the wall.
Solution 2: Calculate collision positions and normals
The behavior you are trying to achieve seems to be "wall-sliding". This means, if you walk into a wall, you will slide along it depending on your movement direction.
The way this is achieved in commercial games, is by calculating more information on the collision. In your case, you just "calculate" the answer to the question "Am I colliding or not?". Information that is required to solve your problem in a more elegant manner is "At what position did I collide?" and "What is the normal of the surface I collided with?". This information can be obtained for example by raycasting your object's bounding box against collision geometry (e.g. by line-raycasting the configuration space obstacle (CSO)).
Using this information, you can resolve you collisions in the following way:
- Calculate your motion_vector as (velocity_x, velocity_y)
- Perform a ray-cast along ths motion vector to obtain collision_position and collision_normal
- Move your object to the collision_position (or slightly before it) so that it doesn't intersect with other objects anymore
- Calculate your new motion_vector, as your current motion_vector minus the component of your motion_vector parallel to the collision_normal
- Shorten the length of your motion_vector by the distance you've already moved
- Repeat steps 2 through 5 until the object managed to move the full length of the motion_vector
While this solution is more complicated, it will resolve the two primary problems with the simple approach (objects leaving a gap inbetween them upon colliding and not being able to cope with non-rectangular bounding boxes).
In summary: the easiest way to solve the problem for your use-case is to separate your movement axes. The way it is solved in commercial games involves calculating more information such as the collision position and the collision normal.