2

Let's say that I have a chatroom application that manages rooms, users, and messages. I'm building this out as an opportunity to practice some service/manager/web separation and teach myself good application design.

With the following classes:

Model:

public class Room {
    private final String id = UUID.randomUUID().toString();

    private boolean open = true;

    private List<Message> messages = new ArrayList<Message>();

    public void addMessage(Message m) {
        synchronized(messages) {
            messages.add(m);
        }
    }

    public void close() {
        this.open = false;
    }

    public List<Message> getMessages() {
        synchronized(messages) {
            return new ArrayList<>(messages);
        }
    }
}

Manager:

public interface RoomManager {
    public void add(Room r);
    public Room get(String id);
}

Service:

public class RoomService {
    public void postMessage(Message m, Room r) {
        if (!r.isOpen()) {
            throw new IllegalArgumentException("Room " + r + " is closed");
        }
        r.addMessage(m);
    }
}

The Message class is unexciting - a String contents and maybe a timestamp.

What I'm struggling with is how to layout this design and hopefully answer a few niggling questions that are plaguing me.

Assuming that this above framework will be used in a web service with multiple incoming requests:

  1. A web request to post a message to a room will only have the ID for the room. Should the lookup (translation from String roomId -> Room room) happen at the web layer? Or should the service method be altered to accept the id of the Room instead of the Room itself?

  2. There's a race condition in the postMessage() method wherein the status of the room might change between checking it, and actually posting the message. Is the service level the right place for this synchronization to take place?

  3. I ideally want the service to not have to know what kind of manager it's dealing with - whether it's a database, an in-memory solution, etc. Where should thread safety be determined, at the model, manager, or service level? If I add @Transactional to the service level, it won't help when dealing with an in-memory manager. But adding some kind of object-based synchronization to the service might unnecessarily prevent multiple database queries (that are otherwise unrelated - ie. posting two messages to two separate rooms) from running in parallel. Obviously if we're dealing with a database query, thread safety at the model level is a moot point, since unless we're dealing with a local cache or in-memory model manager, I'm likely to get two separate instances to the same object.

These seem like fairly fundamental questions but I'm having a hard time finding books, tutorials, talks, etc. that answer these kinds of higher-level design questions. I understand concurrency and the various tools (re-entrant locks, synchronized blocks, etc.) but putting them into practice in a sane, manageable way seems to be a DIY endeavor.

Hoping someone can help answer the above questions, or provide some resources that might help me answer them myself.

1 Answer 1

2

There isn't one correct answer for any of the questions you have asked. Architecture, concurrency, performance, programming languages, etc, it's all a matter of trade-offs. Experience is the thing that helps you recognize them and decide upon them. And experience comes from learning, doing, learn from doing, make mistakes, learning from those mistakes, then repeat.

And on top of that there is also the "time" factor. Time changes things, especially in software. Make a nicely designed application now, and in a few years some feature might be needed for which the current design isn't the best to have and needs to be changed or completely replaced.

All that books, talks, tutorials, etc, can do is make you aware of some of these issues before you have to painfully find out about them yourself, and give you a good fundamental and toolbox to start designing your applications.

I'll try to add my view of things to the questions you asked. Without knowing more about your application, beyond the few lines of code you added, these are just opinions:

(1) This depends. Should the web layer know how to retrieve a room based on the id? Why does this have to happen at the web layer? Shouldn't your service layer know how to handle the room ids and transform them into room instances? Isn't your RoomManager doing that? Isn't the RoomManager part of your service layer?

(2) Maybe, maybe not. How about if the room decides on accepting or rejecting a message?

public class Room {
    ....
    public void addMessage(Message m) {
        if (this.open) {
            synchronized (messages) {
                messages.add(m);
            }
        } else {
            throw new IllegalArgumentException("Room " + this + " is closed");
        }        
    }
    ....
}

Your service then just posts the message.

One other observation, how is the room closed? Is this subject to concurrency issues? Should the open flag be volatile or not?

(3) This is a large topic. In most applications, programmers get to cheat. They keep controllers, services, repositories, etc stateless, then delegate the concurrency issues to the database which is well equipped to handle them. When you don't have a transactional store of data, you have to handle concurrency in the layers above. And now trade-offs appear. You might choose to synchronize a lot to ensure consistency, or relax a bit the syncronization to get better performance. One is easy to do but might be slow, the other is hard to do but might be faster. Here, the language can help by providing good thread safe and performant data structures. You might delegate some of the syncronization to them. Another way could be to forgo syncronization all together at the service layer and have an injectable persistence layer. For a database the implementation can use @Transactional, for an in memory store you can use java.util.concurrent classes, etc.

If you are doing this for learning then decide on what you think is best now, then iterate. Implement a solution then try to change some layer or extend it with some features. Where does it break? Where are things hard to change? etc. Then try finding solutions to those problems.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.