1

I've started work on a web application personal project that is more a learning opportunity more than anything and I'm brainstorming the stack. My number one priority is to not overcomplicate things especially splitting up things into too many services. My second priority is to make sure that if, necessary, the entire thing can be scaled up without any bottlenecks.

I've settled on the following stack

stack

Unfortunately there's one bottleneck that I can't remove - at least without prohibitively overcomplicating things - the Game Engine which is meant to store a copy of and simulate all game state as well as dealing with actions/tasks.

For this reason, for read-only queries, I want the API Endpoints (which will be only one service that can have multiple instances for now) to communicate directly with the database rather than going through the Messaging Queue or Game Engine. This is so that the game engine doesn't have to deal with database queries as well as simulation.

The connection would basically just be putting in the IP address/port of the db using some C++ PostgreSQL library and writing SQL queries manually.

However, now I'm wondering if I'm going about this the wrong way. Possible concerns I have are,

  • Is writing SQL queries in the API Endpoints service the best idea?

  • Should I have another service that deals with just read queries to the database?

  • Are there scalability issues that I'm not seeing?

Any help, or insight would be appreciated, even if it's to say my approach is completely incorrect.

4 Answers 4

2

Disclaimer: For simplicity, whenever I refer to "architecture", I will refer to the solution as a whole (the sum of all the components: APIs, queues, DBs, etc.) Whenever I refer to the "micro-architecture", I will refer to the internal layout of the application. For example, the API's layers


Is writing SQL queries in the API Endpoints service the best idea?

Most API implementations I have seen or implemented myself do calls to DB. Of course, the DB connections and calls are not programmed in controllers. They are implemented in the data access layers usually.

The following micro-architecture is somewhat popular among Web developments.

(Endpoints) Controllers -> (Business) Services -> (Data Access) [Repositories -> Data Sources]

Should I have another service that deals with just-read queries to the database?

I wouldn't recommend it.

In my experience, connecting to a database through a remote standalone service is overkill1. It's easy to end up making a sort of online DB client, but considerably less performant, efficient and flexible than the DB driver the service is wrapping up.

It would be simpler to split API endpoints into two different standalone APIs: /engine and /stats.

The API /engine focuses on handling and dispatching commands to the engine. Its micro-architecture changes very little. Presumably, the code related to DB queries will be transferred to the 2nd API.

The API /stats focuses on the DB queries the front end needs. Its micro-architecture can be like the one I introduced earlier in the first section. Allowing the API to connect and call the DB w/o a (standalone) service in between simplifies the system architecture considerably. The API won't be constrained/locked to a model imposed by another (standalone) service, allowing us to search, fetch and format in the way that best suits our needs w/o unnecessary hurdles (additional mappings, serialization and deserialization, SDLC dependencies). The latency will be lower too.

Are there scalability issues that I'm not seeing?

It should be fine. Moreover, it should be possible to combine horizontal and vertical.

  • Horizontal:

    • Deploying new instances of /stats to handle the load strictly related to DB queries.
    • Deploying new instances of /engine to handle the load strictly related to game actions, events, effects, etc.
  • Vertical

    • Size /stats connection and thread pools according to the hardware (cores). Choose suitable storage for faster reads and writes to disk (temporal data), and size the RAM according to the needs for in-memory cache, table lookups, etc.
    • Size /engine thread pools according to the hardware (cores).
    • Choose the best CPU possible for I/O (many but short CPU bursts)

By mixing up both, we get the possibility to implement fine-grained scalability policies by segments. For example

  • Horizontal: 1 or more instances of /stats and /engine per agent type (client): web browser, Android, iOS, desktop, ...
  • Vertical: Each instance is sized according to the expected per-agent load. For example, instances backing iOS & Android clients might have better CPUs and more RAM than those backing desktop clients because mobile clients generate load pikes more frequently than desktop users, and you want to handle peaks without causing new instance deployments. Peak loads are usually short, and deploying instances to last a few seconds is overkill.

You can scale the strategy to clusters too. For example, by geographical area and environment: NA-test, NA-pro, EU, Asia, etc... Clusters have as many API and Engine instances as deemed adequate.

Note that the engine remains the only authority to change data on the DB. The API /stats execute (only) read-only transactions to DB, so it's fine connecting to the DB w/o an intermediary (authority). The read-only can be enforced from the DB itself, for example limiting the privileges of the user|schema, limiting the access to tables|collections or, connecting to read-only nodes (in replica set configurations)

Considerations

Since it's a pet project and the main goal is learning, don't be afraid of experimenting with different architectures. I may disagree with @LoztInspace, but it doesn't mean (s)he's wrong. I agree with @GranmasterB; the easiest solution would be a monolith, however, I assumed you had reasons to make it as is.


1: A limiting, constraining and complex way to connect to a database.
8
  • Thank you so much for your detailed answer, I'm a bit confused on some parts and was hoping you could clarify,
    – nreh
    May 24, 2023 at 23:45
  • If by service you mean a new layer in the current API, then yes. If by service you mean a new standalone/remote service, then no. Each layer in the diagram corresponds to a different application running standalone. Am I right in assuming you mean that I should include some abstraction of a service within in the API Endpoint service itself? Rather than a separate standalone C++ application?
    – nreh
    May 24, 2023 at 23:46
  • It's simpler to split the current API engine into API game/engine and API game/stats and perform queries from API game/stats... This part is the main part that confuses me. When you say API Engine, I'm assuming you mean the API Endpoint service. What do you mean when you say 'separate it between /engine and /stats'. Do you mean to abstract this within the codebase/api-routes? Or do you mean two separate standalone api services within the API endpoints layer rather than just one (but doesn't this contradict your previous point)?
    – nreh
    May 24, 2023 at 23:46
  • cosmicpython.com/book/part1.html I found this book online describing what I think you mean. If you look at Figure 1, is that roughly what you're talking about when splitting services? In that case, it's probably my fault for the confusion as I didn't really know about this abstraction process and treated 'service' as a standalone application rather than a component in an abstract model which seems to be industry standard.
    – nreh
    May 25, 2023 at 0:00
  • 1
    @nreh Apologizes, sometimes, I struggle with explaining myself in English. I edited the answer and I have tried to make it more concise. Regarding your comments: #1) Yes. I have "drawn" the canonical layout, but you can make it simpler (controller -> repository). #2) Two separate standalone APIs. #3) Exactly, the article proposes the kind of internal layout I'm suggesting for /stats. The word service can be confusing when we don't clarify the scope (high level: Standalone app, low level: layer/component of the application).
    – Laiv
    May 25, 2023 at 9:25
1

If it were me I'd have another data service for the API to call (your middle dot point).

Here's some of the reasons why:

  • DMZ->internal network reasons/security
  • Abstract data details from API shape (though this occurs naturally anyway)
  • Apply other logic that is irrelevant to your API needs or uses more data sources
  • If it becomes more complex you can scale that part without scaling the API part
  • You may end up with data requirements that are not accessible from (that) API so you'd end up doing a bunch of DB work again in another place.

You're probably fine doing API->DB in some cases too, but I probably would not.

1

Many years ago I built something similar to this, however I made several different choices:

  • The UI was Flash based (yeah it was many years ago) - however it maintained a persistent connection to the game engine - in modern times that would probably be a Websocket or similar.
  • We decided we didn't care about HA for a particular game (if a server running a set of game engines died we would just cancel those games) **.
  • We had logs for what happened in each game - but we didn't record much of the game state to the database, just players starting the game and final outcomes and scores.

The game itself was turn based (so it didn't require much memory or CPU) - the key requirement was that as soon as one player took an action other players were notified immediately - bluntly games like this have roughly the same footprint as a chat server - which is why we choose this design.

Also of note was that we ran a single process to service multiple games, again due to the limited resource requirements that each game had.

With this architecture we didn't need a message queue.

** - Technically it was possible to recreate the game state from the logs, but we never got round to implementing that.

--

Most of the non "in game" system requirements could broadly be classified as "MatchMaking" so I would suggest creating a match making service to deal with everything else.

1

Yes, it's OK. Very common. I'd go so far as to say if you don't actually need the messaging queue - which you probably don't because you say this is a personal project - get rid of it because its just over-complicating things right now. (Unless of course you're specifically trying to implement one)

1
  • 1
    Well, since it's for learning, there are a lot of lessons to learn from distributed architectures like the one OP has right now. Maybe, the first one is, too much distribution can kill you. Or some things are worth being segregated more than others.
    – Laiv
    May 25, 2023 at 15:53

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.