A food delivery order is born when you tap "Place order". For the next 40 minutes it is busy: the restaurant accepts it, a rider picks it up, the app updates its status. Once it is delivered, nobody changes it any more, and some time later the system clears it from memory. Every object in your program goes through the same kind of journey. Understanding that journey, and who is responsible for each step of it, is what this chapter is about.
Object lifecycle = the journey of an object from the moment it is created, through the time it is used, until it is cleaned up and its memory is given back.
Real-world analogy: Think of a library membership card. It is issued after your details are checked (creation), you use it to borrow and return books (usage), and when you leave the city you surrender it so the library can close your account and reuse the card number (cleanup). A card that was never surrendered keeps your account open forever, even though nobody uses it. In code, that is a memory or resource leak.
The phases every object goes through
1. What Is the Object Lifecycle?
Every object passes through the same broad phases:
- Creation. Memory is reserved and the constructor runs. Its job is to put the object into a valid starting state.
- Usage. Other code calls the object's methods. Its state changes over time, and other objects may hold references to it.
- End of use. The object goes out of scope, or the last reference to it is removed. From now on, nobody can reach it.
- Cleanup. Resources the object was holding (a file, a database connection, a lock) are released.
- Memory is reclaimed. In C++ this happens immediately when the object is destroyed. In Java, Python and JavaScript the runtime frees the memory automatically, at a time it chooses.
In Low Level Design you think about the lifecycle at two levels:
- The language level: when is the constructor called, when is the object destroyed, and how do you make sure cleanup happens?
- The domain level: which business states does the entity go through (an order goes from
PLACEDtoDELIVERED), and which moves between states are allowed?
Both matter in interviews. A Parking Lot, Food Delivery or Library design is judged on whether its objects are created by the right class, move through sensible states, and release what they hold.
2. Creation: How an Object Is Born
Creation is handled by the constructor. A good constructor does three things:
- Sets every field to a meaningful starting value.
- Checks the input and refuses to build an invalid object (by throwing an exception).
- Builds the parent part first when the class extends another class.
Here is an electric scooter in a bike-rental app. A scooter is a Vehicle, and every vehicle needs a number plate. The Vehicle constructor rejects an empty plate, and ElectricScooter decides its starting status from the battery level.
Output
Creating scooter TS09EV1234...
1. Vehicle part ready: TS09EV1234
2. ElectricScooter part ready: battery 85%, status AVAILABLE
Creating scooter TS09EV5678...
1. Vehicle part ready: TS09EV5678
2. ElectricScooter part ready: battery 12%, status NEEDS_CHARGING
Creating scooter with empty plate...
Creation failed: plate is required
Three things to notice:
- Parent first, child second. Line 1 is always printed before line 2. A child object is built on top of a fully built parent, in every language. In Java and JavaScript the call to
super(...)must come before the child usesthis. In Python you callsuper().__init__(...)yourself. In C++ the base class constructor runs before the derived class's member initialisers. - A failed constructor means no object. For the empty plate, the exception stops construction, so no half-built scooter ever exists in the program. That is far safer than creating the object and calling a separate
init()later, because nobody can forget to call it. - The starting state is decided once. A scooter with 12% battery starts as
NEEDS_CHARGING. The rest of the system never sees a scooter without a status.
Constructor order: the parent part is checked and built before the child part
Where objects live after creation:
- C++ gives you a choice. A stack object (
ElectricScooter s("TS09EV1234", 85);) is destroyed automatically when its block ends. A heap object made withnewlives until someone deletes it. In modern C++ you let a smart pointer such asstd::unique_ptrdo the deleting. - Java, Python and JavaScript always create objects on the heap and give you a reference to them. The object lives as long as at least one reference to it can still be reached.
3. Usage: The Object Moves Through States
Once created, most business objects do not just sit there. They move through a series of states, and only some moves make sense. A delivered order cannot go back to the kitchen. An order that is already out for delivery cannot be cancelled by the customer. If the class does not enforce these rules, every caller has to remember them, and sooner or later one of them gets it wrong.
Allowed states of a FoodOrder. DELIVERED and CANCELLED are final states
The clean approach is to keep the allowed moves inside the class itself and let one method, moveTo(), be the only way to change the status:
Output
Order #101 created in state PLACED
#101: PLACED -> PREPARING
#101: PREPARING -> OUT_FOR_DELIVERY
#101: cannot go from OUT_FOR_DELIVERY to CANCELLED
#101: OUT_FOR_DELIVERY -> DELIVERED
Order #101 closed: true
#101: cannot go from DELIVERED to PREPARING
- The rules live in one place. The
ALLOWEDtable is the whole business lifecycle of an order. Adding a new state such asREADY_FOR_PICKUPmeans changing one table, not hunting through the codebase. - Invalid moves are rejected instead of silently corrupting the object. Here they print a message. In a real system they would usually throw an exception.
- Final states end the business lifecycle. After
DELIVERED, the order is read-only. It may stay in memory (or in the database) for history, but it is "closed".
When the behaviour in each state becomes complex, this grows into the State design pattern, where each state is its own class. For most LLD problems, a transition table like this one is enough.
4. Destruction: How an Object Ends
This is where the four languages differ the most.
When an object is cleaned up, and the cleanup tool to use, in each language
| Language | When is the object destroyed? | Cleanup hook | Reliable way to release resources |
|---|---|---|---|
| C++ | Exactly when a stack object goes out of scope, or when a heap object is deleted | Destructor ~ClassName() | RAII: acquire in the constructor, release in the destructor |
| Java | Some time after it becomes unreachable, when the garbage collector decides to run | None you should rely on (finalize() is deprecated) | AutoCloseable + try-with-resources |
| Python | Usually as soon as the last reference is gone (reference counting); cycles are freed later by the cycle collector | __del__, but it is not guaranteed to run | Context manager + with block |
| JavaScript | Some time after it becomes unreachable, when the garbage collector runs | None (FinalizationRegistry callbacks may never run) | An explicit close() in try / finally |
The key lesson: memory is freed for you, but other resources are not. A garbage collector reclaims memory. It does not close a file, return a database connection to a pool, or release a lock at a predictable time. For those, you need cleanup that runs exactly when you are done, even if an error happens along the way.
5. Guaranteed Cleanup, Even When Something Fails
A connection pool has a fixed number of database connections. Every job borrows one and must give it back. If a job crashes halfway and forgets to return the connection, the pool slowly runs dry and the whole app hangs. Here the borrowing is tied to the lifetime of a small Lease object, and each language's cleanup tool guarantees the connection is returned:
Output
Pool ready: 2 free
[report] got conn-1 (free: 1)
[report] running query
[report] returned conn-1 (free: 2)
[payment] got conn-1 (free: 1)
[payment] running query
[payment] returned conn-1 (free: 2)
[payment] failed: gateway timeout
Free connections at the end: 2
Look at the order of the last three lines for the payment job. The connection is returned before the error reaches the caller. That is exactly what you want: the cleanup runs on the way out, whether the block ended normally or because of an exception.
The lease gives the connection back before the error is passed on to the caller
- C++ (RAII): the destructor of
Leaseruns whenrunJobexits, including during exception unwinding. Copying is deleted so two leases can never return the same connection twice. - Java: any class that implements
AutoCloseablecan be used in try-with-resources, and itsclose()is always called. - Python:
__exit__runs when thewithblock ends. ReturningFalselets the exception carry on to the caller. - JavaScript: the
finallyblock always runs. Newer runtimes also supportusingdeclarations withSymbol.dispose, which work like try-with-resources.
6. Ownership: Who Ends an Object's Life?
In a garbage-collected language an object stays alive as long as anything can still reach it. That is convenient, but it also means a forgotten reference keeps an object alive forever. This is the most common "memory leak" in Java, Python and JavaScript: not lost memory, but memory that is still referenced by mistake.
Output
Buggy manager: 3 users logged in
Buggy manager after 2 logouts: holding 3 session(s)
Fixed manager: 3 users logged in
Fixed manager after 2 logouts: holding 1 session(s)
The buggy manager only marks a session as inactive. The Session object is still inside the map, so it can never be freed. On a server with thousands of logins a day, that map grows until the server runs out of memory. The fixed manager removes the entry, so nothing points to the old sessions any more and the runtime can reclaim them. In C++ the same fix also runs the Session destructor immediately, because the map's unique_ptr deletes the object when the entry is erased.
An object can be freed only when nothing refers to it any more
In LLD, always decide for each class:
- Who creates it? Usually the class that owns it, or a factory. A
ParkingLotcreatesTickets, aCartcreates itsCartItems. - Who is allowed to keep a reference to it? Long-lived objects such as caches, registries, singletons and event listener lists are the usual places where references pile up.
- What ends its life? A logout, a completed payment, a ticket being paid for at the exit gate. Make that event remove the object from every place that holds it.
These ownership questions lead straight into the class relationships in the next chapters: in association and aggregation the objects live independently, while in composition the owner controls the life of its parts.
7. Common Mistakes
- Doing work outside the constructor that must always happen. A separate
init()method that callers must remember to call leaves half-built objects around. Validate and set up everything in the constructor. - Relying on the garbage collector to release resources. Files, sockets, connections and locks need explicit cleanup with RAII, try-with-resources,
withorfinally. - Using
finalize()or__del__for important cleanup. They may run late or not at all. - Forgetting to remove objects from long-lived collections (caches, maps, listener lists). This keeps them alive forever.
- Letting any caller change an entity's state freely. Put the allowed transitions inside the class.
- In C++, using raw
newanddeletein normal code. Prefer stack objects and smart pointers, so destruction happens automatically.
8. Summary
| Phase | What happens | What you design |
|---|---|---|
| Creation | Memory is reserved, the constructor runs (parent part first) | Validation and a valid starting state |
| Usage | Methods are called and the state changes | Allowed state transitions inside the class |
| End of use | Out of scope, or the last reference is removed | Clear ownership: who keeps references and who removes them |
| Cleanup | Resources are released | RAII, try-with-resources, with, or try / finally |
| Memory reclaimed | C++: immediately. Java/Python/JS: by the runtime | Nothing, as long as no stray references remain |
Objects that are created valid, change state only through allowed moves, and release what they hold exactly when they are done make an LLD design reliable and easy to reason about.